Understand Clerk pricing and optimize costs. Clerk charges by Monthly Active Users (MAU). Covers pricing tiers, MAU reduction strategies, caching to reduce API calls, and usage monitoring.
| Plan | Price | MAU Included | Extra MAU |
|---|---|---|---|
| Free | $0/mo | 10,000 MAU | N/A |
| Pro | $25/mo | 10,000 MAU | $0.02/MAU |
| Enterprise | Custom | Custom | Custom |
Key pricing concepts:
// Strategy 1: Defer authentication — don't force sign-in until necessary
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const requiresAuth = createRouteMatcher([
'/dashboard(.*)',
'/settings(.*)',
'/api/protected(.*)',
])
export default clerkMiddleware(async (auth, req) => {
// Only require auth for specific routes (not entire site)
if (requiresAuth(req)) {
await auth.protect()
}
})
// Strategy 2: Use anonymous access for read-only features
// app/blog/[slug]/page.tsx
import { auth } from '@clerk/nextjs/server'
export default async function BlogPost({ params }: { params: { slug: string } }) {
const { userId } = await auth() // Check but don't require
const post = await db.post.findUnique({ where: { slug: params.slug } })
return (
<article>
<h1>{post?.title}</h1>
<div>{post?.content}</div>
{userId ? <CommentForm /> : <p>Sign in to comment</p>}
</article>
)
}
// lib/user-cache.ts
import { cache } from 'react'
import { currentUser } from '@clerk/nextjs/server'
// Deduplicate within single request (free)
export const getUser = cache(async () => {
return currentUser()
})
// Cross-request caching reduces Backend API calls
import { unstable_cache } from 'next/cache'
import { clerkClient } from '@clerk/nextjs/server'
export const getUserMetadata = unstable_cache(
async (userId: string) => {
const client = await clerkClient()
const user = await client.users.getUser(userId)
return user.publicMetadata
},
['user-metadata'],
{ revalidate: 600 } // 10-minute cache
)
// app/api/admin/clerk-usage/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
export async function GET() {
const { has } = await auth()
if (!has({ role: 'org:admin' })) {
return Response.json({ error: 'Admin only' }, { status: 403 })
}
const client = await clerkClient()
const users = await client.users.getUserList({ limit: 1 })
return Response.json({
totalUsers: users.totalCount,
// Estimate MAU based on recent sign-ins
estimatedMAU: 'Check Clerk Dashboard > Billing for actual MAU',
dashboardUrl: 'https://dashboard.clerk.com/last-active?after=30d',
})
}
// scripts/cleanup-inactive-users.ts
import { createClerkClient } from '@clerk/backend'
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! })
async function findInactiveUsers(daysInactive = 90) {
const cutoff = Date.now() - daysInactive * 24 * 60 * 60 * 1000
const allUsers = await clerk.users.getUserList({ limit: 500 })
const inactive = allUsers.data.filter(
(user) => (user.lastSignInAt || 0) < cutoff
)
console.log(`Found ${inactive.length} users inactive for ${daysInactive}+ days`)
console.log('Consider: notification campaign, data export, or account cleanup')
return inactive
}
findInactiveUsers()
| Issue | Cause | Solution |
|---|---|---|
| Unexpected bill increase | MAU spike from bot traffic | Add bot detection, restrict auth to needed routes |
| Feature limitations | Free tier limits (no SSO, etc.) | Upgrade to Pro ($25/mo) |
| High API call volume | No caching | Add React cache() + unstable_cache() |
| MAU count mismatch | Counting test users | Use separate dev instance (free, unlimited) |
function estimateMonthlyCost(mau: number): string {
if (mau <= 10_000) return 'Free tier ($0/mo)'
const overage = mau - 10_000
const cost = 25 + overage * 0.02
return `Pro tier: $${cost.toFixed(2)}/mo (${overage.toLocaleString()} extra MAU at $0.02 each)`
}
console.log(estimateMonthlyCost(15_000)) // "Pro tier: $125.00/mo (5,000 extra MAU at $0.02 each)"
console.log(estimateMonthlyCost(50_000)) // "Pro tier: $825.00/mo (40,000 extra MAU at $0.02 each)"
Proceed to clerk-reference-architecture for architecture patterns.