CentercodePorygon
DittoPorygonAPI

Rate Limiting

Request throttling with Upstash Redis

SafeReliable

Overview

Use Upstash Redis for rate limiting with preset configurations. Protects against abuse and ensures fair resource usage.

What it is

Sliding window rate limiting with configurable presets (strict, standard, relaxed, ai) and per-user/per-IP tracking.

Why we use it

Prevent brute force attacks, protect expensive operations, and ensure service availability under load.

When to use

All API endpoints, especially authentication, AI generation, and any resource-intensive operations.

Key Features

  • Preset configurations (strict, standard, relaxed, ai)
  • Per-user or per-IP rate limiting
  • Sliding window algorithm
  • Rate limit headers for transparency

Quick Start

Apply Rate Limiting

Add rate limiting to an API route.

// Apply rate limiting to an API route
import { rateLimit } from '@/lib/middleware/security';
import { handleError } from '@/lib/errors';

export async function POST(request: Request) {
  try {
    // Check rate limit before processing
    const rateLimitResult = await rateLimit(request, 'standard');

    if (!rateLimitResult.success) {
      return NextResponse.json(
        { ok: false, error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
        {
          status: 429,
          headers: {
            'Retry-After': String(rateLimitResult.reset),
            'X-RateLimit-Limit': String(rateLimitResult.limit),
            'X-RateLimit-Remaining': String(rateLimitResult.remaining),
          }
        }
      );
    }

    // Process request...
    return NextResponse.json({ ok: true, data: result });
  } catch (error) {
    return handleError(error);
  }
}

Patterns

Rate Limit Presets

Choose appropriate limits by endpoint type.

// Rate limit presets
import { rateLimit, RATE_LIMIT_PRESETS } from '@/lib/middleware/security';

// Available presets:
// 'strict'   - 10 requests per minute (login, password reset)
// 'standard' - 60 requests per minute (general API)
// 'relaxed'  - 300 requests per minute (read-heavy endpoints)
// 'ai'       - 20 requests per minute (AI generation)

// Login endpoint - strict limiting
export async function POST(request: Request) {
  const result = await rateLimit(request, 'strict');
  // ...
}

// List endpoint - relaxed limiting
export async function GET(request: Request) {
  const result = await rateLimit(request, 'relaxed');
  // ...
}

Per-User Limiting

Rate limit by user ID when authenticated.

// Per-user rate limiting
import { rateLimit } from '@/lib/middleware/security';
import { auth } from '@/lib/middleware/auth';

export async function POST(request: Request) {
  const session = await auth();

  // Rate limit by user ID if authenticated, IP otherwise
  const identifier = session?.user.id ?? getClientIP(request);

  const result = await rateLimit(request, 'standard', {
    identifier,
  });

  if (!result.success) {
    return NextResponse.json(
      { ok: false, error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
      { status: 429 }
    );
  }

  // Process request...
}

Rate Limit Headers

Include headers for client transparency.

// Rate limit response headers
// Always include these headers for client transparency

const headers = {
  'X-RateLimit-Limit': String(limit),      // Max requests allowed
  'X-RateLimit-Remaining': String(remaining), // Requests remaining
  'X-RateLimit-Reset': String(reset),      // Unix timestamp when limit resets
  'Retry-After': String(retryAfter),       // Seconds until retry allowed
};

return NextResponse.json(data, { headers });

Watch Out

No rate limiting on sensitive endpoints

Don't

// No rate limiting on sensitive endpoint
export async function POST(request: Request) {
  const { email, password } = await request.json();

  // Allows unlimited login attempts - vulnerable to brute force!
  const user = await attemptLogin(email, password);
  return NextResponse.json({ ok: true, data: user });
}

Do

// Rate limited sensitive endpoint
export async function POST(request: Request) {
  const rateLimitResult = await rateLimit(request, 'strict');

  if (!rateLimitResult.success) {
    return NextResponse.json(
      { ok: false, error: { code: 'RATE_LIMITED', message: 'Too many attempts' } },
      { status: 429, headers: { 'Retry-After': '60' } }
    );
  }

  const { email, password } = await request.json();
  const user = await attemptLogin(email, password);
  return NextResponse.json({ ok: true, data: user });
}

Overly strict limits affecting UX

Don't

// Overly strict limits on read endpoints
export async function GET(request: Request) {
  // 10 requests/minute for a read endpoint is too strict
  const result = await rateLimit(request, 'strict');

  // Users will constantly hit rate limits during normal use
  // ...
}

Do

// Appropriate limits based on endpoint type
export async function GET(request: Request) {
  // 300 requests/minute for read endpoints
  const result = await rateLimit(request, 'relaxed');
  // ...
}

export async function POST(request: Request) {
  // 60 requests/minute for standard mutations
  const result = await rateLimit(request, 'standard');
  // ...
}
  • Missing bypass for internal services
  • Not including rate limit headers

Related

Security Headers

Security headers

API Architecture

API patterns

Authentication

Session management