| [ Web Proxy ] |
| Viewing: https://marrowstack.dev/docs/blocks/ratelimit | [Back] [Original] |
Utility Starter
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.
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).
No dependencies required for the in-memory mode. For Upstash Redis:
npm install @upstash/redis| Variable | Required | Default | Purpose |
|---|---|---|---|
| UPSTASH_REDIS_REST_URL | no | Upstash Redis REST URL. When set, Redis is used instead of in-memory store. | |
| UPSTASH_REDIS_REST_TOKEN | no | Upstash 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.
Wrap any API route handler with withRateLimit():
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'
}
)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
},
}
)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.`)
}limit: 2, window: 10 temporarily and call the route 3 times confirm the third returns 429 Too Many Requests.RateLimit-Limit, RateLimit-Remaining, and Retry-After are present.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.
| Web Proxy Viewer | New URL | Original Page |