Utility · Starter

Rate Limiting

Sliding-window rate limiting for Next.js 14+ API routes — in-memory store for development, drop-in Upstash Redis upgrade for production, with per-IP and per-user strategies and standard RateLimit-* response headers.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the Rate Limiting block, and click Copy all files. Paste lib/ratelimit.ts into your project.

Out of the box the block uses an in-process memory store — no external dependencies required. When you're ready for production, swap in Upstash Redis by setting two environment variables (see section 4).

2. Prerequisites

  • Next.js 14 or 15, App Router
  • TypeScript 5+
  • Upstash Redis account (optional — only for production Redis backend)

3. Install

No dependencies required for the in-memory mode. For Upstash Redis:

bash
npm install @upstash/redis

4. Environment

VariableRequiredDefaultPurpose
UPSTASH_REDIS_REST_URLnoUpstash Redis REST URL. When set, Redis is used instead of in-memory store.
UPSTASH_REDIS_REST_TOKENnoUpstash Redis REST token. Required alongside UPSTASH_REDIS_REST_URL.

When neither variable is set, the block falls back to an in-process sliding-window store. The in-memory store is not shared across serverless instances — use Redis in production.

5. Wire it in

Wrap any API route handler with withRateLimit():

app/api/contact/route.ts
import { withRateLimit } from '@/lib/ratelimit'
import { NextRequest, NextResponse } from 'next/server'

export const POST = withRateLimit(
  async (req: NextRequest) => {
    // your handler logic here
    return NextResponse.json({ ok: true })
  },
  {
    limit: 5,        // max requests
    window: 60,      // per 60 seconds
    strategy: 'ip',  // 'ip' | 'user' | 'global'
  }
)
Per-user rate limiting
import { withRateLimit } from '@/lib/ratelimit'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'

export const POST = withRateLimit(
  async (req) => {
    const session = await getServerSession(authOptions)
    // handler...
    return Response.json({ ok: true })
  },
  {
    limit: 20,
    window: 3600,       // 20 requests per hour
    strategy: 'user',   // uses session user ID as the key
    getUserId: async (req) => {
      const session = await getServerSession(authOptions)
      return session?.user?.id ?? null
    },
  }
)
Reading rate limit headers client-side
const res = await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) })

// Standard rate limit headers are set on every response
const limit     = res.headers.get('RateLimit-Limit')     // e.g. "5"
const remaining = res.headers.get('RateLimit-Remaining') // e.g. "4"
const reset     = res.headers.get('RateLimit-Reset')     // Unix timestamp

if (res.status === 429) {
  const retryAfter = res.headers.get('Retry-After')
  console.log(`Rate limited. Retry after ${retryAfter}s.`)
}

6. Verify it works

  1. Set limit: 2, window: 10 temporarily and call the route 3 times — confirm the third returns 429 Too Many Requests.
  2. Inspect the response headers and confirm RateLimit-Limit, RateLimit-Remaining, and Retry-After are present.
  3. Wait for the window to expire and confirm requests succeed again.
  4. If using Upstash: open the Upstash console and confirm sliding-window keys appear under your database.

7. Failure modes & fixes

Rate limit not enforced — all requests pass

Cause: When using the in-memory store, each serverless function instance has its own counter. Two concurrent cold starts each start at 0.

Fix: Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to use a shared Redis store. For local dev, in-memory is fine.

strategy: 'user' always falls back to IP

Cause: getUserId() returned null — session was not available or the user was not signed in.

Fix: Ensure the route is behind an auth check, or handle the null case explicitly to apply a stricter IP-based limit for unauthenticated users.

429 response has no Retry-After header

Cause: An older version of the block file is in use.

Fix: Re-copy lib/ratelimit.ts from the block detail page to get the latest version with standard headers.