The Rate Limiting Problem
No rate limiting = your AI SaaS gets scraped, abused, or accidentally DDoS'd by a runaway script.
One user's infinite loop shouldn't kill service for everyone else.
Here's how to add rate limiting to Next.js API routes without Redis.
Option 1: In-Memory Rate Limiter (No Infrastructure)
Good for: single-instance deployments, dev environments, prototypes.
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
export function rateLimit({
key,
limit = 10,
windowMs = 60_000,
}: {
key: string
limit?: number
windowMs?: number
}) {
const now = Date.now()
const record = rateLimitMap.get(key)
if (!record || now > record.resetTime) {
rateLimitMap.set(key, { count: 1, resetTime: now + windowMs })
return { success: true, remaining: limit - 1 }
}
if (record.count >= limit) {
return {
success: false,
remaining: 0,
resetTime: record.resetTime
}
}
record.count++
return { success: true, remaining: limit - record.count }
}
Using It in API Routes
// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { rateLimit } from '@/lib/rate-limit'
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// Rate limit: 20 AI calls per hour per user
const { success, remaining, resetTime } = rateLimit({
key: `ai-generate:${session.user.id}`,
limit: 20,
windowMs: 60 * 60 * 1000, // 1 hour
})
if (!success) {
const resetIn = Math.ceil((resetTime! - Date.now()) / 1000)
return NextResponse.json(
{ error: `Rate limit exceeded. Try again in ${resetIn}s` },
{
status: 429,
headers: {
'X-RateLimit-Limit': '20',
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(Math.ceil(resetTime! / 1000))
}
}
)
}
// Proceed with AI generation...
return NextResponse.json({ result: 'Generated content' })
}
Option 2: Upstash Redis (Production, Multi-Instance)
For Vercel or any multi-region deployment, in-memory state doesn't persist across instances.
Use Upstash (serverless Redis) instead.
npm install @upstash/ratelimit @upstash/redis
// lib/rate-limit-redis.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
// Sliding window: 10 requests per 10 seconds
export const rateLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true, // Track in Upstash dashboard
})
// More restrictive limiter for expensive AI routes
export const aiRateLimiter = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(20, '1 h'),
analytics: true,
})
// Usage in API route
import { NextRequest, NextResponse } from 'next/server'
import { aiRateLimiter } from '@/lib/rate-limit-redis'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { success, limit, remaining, reset } = await aiRateLimiter.limit(
`ai:${session.user.id}`
)
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(reset)
}
}
)
}
return NextResponse.json({ result: 'AI output' })
}
Middleware-Level Rate Limiting
For blanket protection across all API routes:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'),
})
export async function middleware(request: NextRequest) {
// Only rate limit API routes
if (!request.nextUrl.pathname.startsWith('/api')) {
return NextResponse.next()
}
const ip = request.ip ?? '127.0.0.1'
const { success } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*'
}
Plan-Based Limits
Different limits per subscription tier:
const PLAN_LIMITS = {
free: { requests: 50, windowMs: 24 * 60 * 60 * 1000 }, // 50/day
pro: { requests: 1000, windowMs: 24 * 60 * 60 * 1000 }, // 1000/day
enterprise: { requests: -1, windowMs: 0 }, // unlimited
} as const
export async function planRateLimit(userId: string, plan: keyof typeof PLAN_LIMITS) {
const limits = PLAN_LIMITS[plan]
if (limits.requests === -1) return { success: true, remaining: Infinity }
return rateLimit({
key: `plan:${userId}`,
limit: limits.requests,
windowMs: limits.windowMs
})
}
Ship It Pre-Built
The AI SaaS Starter Kit includes rate limiting pre-configured with both in-memory and Redis options, plan-based limits, and proper HTTP headers.
$99 one-time at whoffagents.com
Top comments (0)