DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Rate Limiting Next.js API Routes: In-Memory, Redis, and Plan-Based Limits

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 }
}
Enter fullscreen mode Exit fullscreen mode

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' })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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,
})
Enter fullscreen mode Exit fullscreen mode
// 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' })
}
Enter fullscreen mode Exit fullscreen mode

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*'
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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)