How to Implement Rate Limiting in Your API

Share
How-To Guide

How to Implement Rate Limiting

Stop abuse before it stops your server

TL;DR

TL;DR

Use Upstash Rate Limit for serverless apps or express-rate-limit for traditional Node.js. Track requests by IP or user ID. Return 429 status when limits are exceeded. Start with conservative limits (100 requests/minute) and adjust based on real usage.

Why Rate Limiting Matters

Without rate limiting, a single user or bot can:

  • Overwhelm your server with requests
  • Run up your API costs (OpenAI, Stripe, etc.)
  • Brute force passwords or API keys
  • Scrape your entire database

Option 1: Upstash Rate Limit (Serverless)

Best for Vercel, Netlify, and other serverless platforms.

1

Install the package

npm install @upstash/ratelimit @upstash/redis
2

Create a rate limiter

// lib/ratelimit.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!,
});

// 10 requests per 10 seconds
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
});
3

Use in your API route

// app/api/protected/route.ts
import { ratelimit } from '@/lib/ratelimit';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  // Get identifier (IP or user ID)
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';

  // Check rate limit
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return Response.json(
      { error: 'Too many requests' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        }
      }
    );
  }

  // Process the request
  return Response.json({ message: 'Success' });
}

Option 2: Express Rate Limit (Traditional)

Best for Express.js and traditional Node.js servers.

1

Install the package

npm install express-rate-limit
2

Create rate limit middleware

import rateLimit from 'express-rate-limit';

// General API limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: { error: 'Too many requests, try again later' },
  standardHeaders: true, // Return rate limit info in headers
  legacyHeaders: false,
});

// Stricter limiter for auth routes
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  message: { error: 'Too many login attempts' },
});

// Apply to routes
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);

Option 3: Next.js Middleware

Apply rate limiting to all routes using middleware.

// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(20, '10 s'),
});

export async function middleware(request: NextRequest) {
  // Only rate limit API routes
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next();
  }

  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Rate Limiting Strategies

Fixed Window

Simple but can allow bursts at window boundaries. Example: 100 requests per minute, resets at the start of each minute.

Sliding Window

Smoother distribution, recommended for most use cases. Looks at the last N seconds continuously.

Token Bucket

Allows controlled bursts while maintaining average rate. Good for APIs that need occasional spikes.

// Different strategies with Upstash
import { Ratelimit } from '@upstash/ratelimit';

// Fixed window: 10 requests per 10 seconds
Ratelimit.fixedWindow(10, '10 s')

// Sliding window: smoother, recommended
Ratelimit.slidingWindow(10, '10 s')

// Token bucket: allows bursts
Ratelimit.tokenBucket(10, '10 s', 5) // 5 tokens max burst

Choosing What to Limit By

IP Address

// Most common, but can affect shared IPs
const ip = request.headers.get('x-forwarded-for') ??
           request.headers.get('x-real-ip') ??
           '127.0.0.1';
const identifier = ip.split(',')[0].trim();

User ID (Authenticated)

// Better for logged-in users
const session = await getSession(request);
const identifier = session?.user?.id ?? ip;

API Key

// For public APIs
const apiKey = request.headers.get('x-api-key');
const identifier = apiKey ?? ip;

Tip: Different Limits for Different Routes

Apply stricter limits to sensitive endpoints like login, password reset, and payment processing. Be more generous with read-only endpoints.

  • General API: 100 requests/minute
  • Login attempts: 5 per hour per IP
  • Password reset: 3 per hour per email
  • File uploads: 10 per hour
  • AI/LLM calls: 20 per minute (cost protection)
  • Public read API: 1000 per minute

Handling Rate Limit Errors

// Client-side handling
async function fetchWithRetry(url: string, options?: RequestInit) {
  const response = await fetch(url, options);

  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000;

    // Show user-friendly message
    throw new Error(`Rate limited. Please wait ${Math.ceil(waitTime / 1000)} seconds.`);
  }

  return response;
}

Testing Rate Limits

# Bash: Send many requests quickly
for i in {1..20}; do
  curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/test
done

# You should see 200s then 429s
How-To Guides

How to Implement Rate Limiting in Your API