logoPressFast

Rate Limiting

Protect your API from abuse with rate limiting

Rate Limiting

Implement rate limiting to prevent abuse and protect your infrastructure.

Why Rate Limiting?

  • Prevent DDoS attacks
  • Protect against brute force attacks
  • Control API costs
  • Ensure fair usage
  • Improve system stability

Upstash Redis

Use Upstash for serverless-friendly rate limiting.

Setup

pnpm add @upstash/ratelimit @upstash/redis

Configure environment variables:

UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...

Basic Implementation

// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

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

API Route Protection

// app/api/protected/route.ts
import { ratelimit } from '@/lib/rate-limit';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';

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

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

  // Process request
  return NextResponse.json({ success: true });
}

Different Strategies

Fixed Window

export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.fixedWindow(10, '60 s'),
});

Sliding Window

export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '60 s'),
});

Token Bucket

export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.tokenBucket(10, '60 s', 100),
});

Per-User Rate Limiting

import { auth } from '@/lib/auth';

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

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const identifier = `user:${session.user.id}`;
  const { success } = await ratelimit.limit(identifier);

  if (!success) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  // Process request
}

Different Limits for Different Tiers

// lib/rate-limit.ts
export const freeTierLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '1 h'),
});

export const proTierLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(100, '1 h'),
});

export const enterpriseLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(1000, '1 h'),
});
export async function POST(request: Request) {
  const session = await auth();
  const user = await db.user.findUnique({
    where: { id: session.user.id },
  });

  let limiter;
  switch (user?.tier) {
    case 'pro':
      limiter = proTierLimit;
      break;
    case 'enterprise':
      limiter = enterpriseLimit;
      break;
    default:
      limiter = freeTierLimit;
  }

  const { success } = await limiter.limit(`user:${user.id}`);

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

  // Process request
}

Middleware Rate Limiting

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

export async function middleware(request: NextRequest) {
  const ip = request.ip ?? 'anonymous';

  // Only rate limit API routes
  if (request.nextUrl.pathname.startsWith('/api/')) {
    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*',
};

Prevent magic link spam:

// app/api/auth/magic-link/route.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const magicLinkLimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(3, '1 h'),
});

export async function POST(request: Request) {
  const { email } = await request.json();

  const { success } = await magicLinkLimit.limit(email);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Too many magic link requests. Please try again later.',
      },
      { status: 429 }
    );
  }

  // Send magic link
}

Password Reset Rate Limiting

const passwordResetLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(3, '1 h'),
});

export async function POST(request: Request) {
  const { email } = await request.json();

  const { success } = await passwordResetLimit.limit(email);

  if (!success) {
    // Don't reveal if rate limit hit for security
    return NextResponse.json({
      message: 'If an account exists, a reset link will be sent.',
    });
  }

  // Send password reset email
}

Error Response Format

interface RateLimitError {
  error: string;
  limit: number;
  remaining: number;
  reset: number; // Unix timestamp
}

// Client can use reset time to show countdown
const secondsUntilReset = Math.ceil((reset - Date.now()) / 1000);

Client-Side Handling

'use client';

import { useState } from 'react';

export function RateLimitedForm() {
  const [error, setError] = useState<string | null>(null);
  const [retryAfter, setRetryAfter] = useState<number | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/endpoint', {
        method: 'POST',
        body: JSON.stringify(data),
      });

      if (response.status === 429) {
        const data = await response.json();
        const secondsUntilReset = Math.ceil((data.reset - Date.now()) / 1000);
        setRetryAfter(secondsUntilReset);
        setError(`Too many requests. Try again in ${secondsUntilReset} seconds.`);
        return;
      }

      // Handle success
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="text-red-500">{error}</div>}
      {/* Form fields */}
      <button type="submit" disabled={retryAfter !== null}>
        Submit
      </button>
    </form>
  );
}

Monitoring

Track rate limit hits:

import * as Sentry from '@sentry/nextjs';

const { success, limit, remaining } = await ratelimit.limit(identifier);

if (!success) {
  Sentry.captureMessage('Rate limit exceeded', {
    level: 'warning',
    extra: {
      identifier,
      limit,
      remaining,
    },
  });
}

Best Practices

  1. Different limits for different endpoints: Critical endpoints need stricter limits
  2. User-based limits: More generous for authenticated users
  3. Clear error messages: Tell users when they can retry
  4. Return rate limit headers: Help clients implement backoff
  5. Monitor and adjust: Review limits based on actual usage
  6. Whitelist trusted IPs: Allow internal services
  7. Use sliding windows: Smoother rate limiting than fixed windows

Next Steps

Rate Limiting