Without limits, one buggy client or one scraper can exhaust your API’s capacity and take it down for everyone. Rate limiting caps how many requests a client can make in a time window, protecting availability, controlling cost, and slowing down abuse like credential stuffing.

What to limit by

  • API key / user ID — fairest for authenticated APIs; limits follow the customer, not the network.
  • IP address — for anonymous endpoints (login, signup, password reset). Beware shared IPs (offices, mobile carriers, NAT) and attackers rotating IPs.
  • Endpoint — expensive operations (search, exports, AI calls) get stricter limits than cheap reads.
  • Combination — e.g. per IP and per account on login endpoints.

Algorithms

Fixed window — count requests per calendar minute; reset at the boundary. Simple, but allows bursts of up to 2× the limit around window edges.

Sliding window — count requests in the last N seconds (exact log or approximated from two fixed windows). Smoother, slightly more work.

Token bucket — a bucket holds up to B tokens and refills at R per second; each request takes a token. Allows short bursts (B) while enforcing a long-term average (R). The most common choice for APIs.

Leaky bucket — requests drain at a fixed rate; excess queues or is rejected. Smooths traffic to the backend (this is what Nginx’s limit_req implements).

Concurrency limits — cap simultaneous in-flight requests rather than rate; protects against slow, expensive calls.

Responding to limited clients

Return 429 Too Many Requests with information the client can use:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Content-Type: application/json

{"error": "rate_limited", "message": "Too many requests, retry in 30 seconds"}

Retry-After is standard; the RateLimit-* headers follow an IETF draft (many APIs also use X-RateLimit-*). Include remaining-quota headers on successful responses too, so well-behaved clients can slow down before hitting the limit.

Where to implement it

At the edge: Nginx

Fast and cheap — rejected requests never reach your app:

limit_req_zone $binary_remote_addr zone=api_ip:10m rate=10r/s;
limit_req_zone $http_x_api_key   zone=api_key:20m rate=50r/s;
limit_req_status 429;

location /api/ {
    limit_req zone=api_ip  burst=20 nodelay;
    limit_req zone=api_key burst=100 nodelay;
    proxy_pass http://127.0.0.1:3000;
}

Limits are per Nginx server, kept in memory. See Nginx rate limiting.

Shared across servers: Redis

With several app servers, counters must be shared. A fixed-window counter in Redis:

INCR rate:{key}:{minute}
EXPIRE rate:{key}:{minute} 60

If the result exceeds the limit, reject. For token buckets or sliding windows, use a small Lua script so the check-and-update is atomic. Libraries exist for most frameworks (express-rate-limit with a Redis store, django-ratelimit, Laravel’s RateLimiter, slowapi for FastAPI).

In the application

Gives the most context — plans, user tiers, per-endpoint costs (e.g. counting tokens for AI endpoints). Combine with an edge limit as a coarse first line of defence.

API gateways

Kong, Tyk, Traefik middlewares, Envoy and managed gateways offer rate limiting as configuration.

Behind proxies and CDNs

Make sure the limiter sees the real client IP, not the proxy’s — otherwise all clients share one bucket. Configure trusted proxies (real_ip_header in Nginx, trust proxy in Express). See real IP behind Cloudflare.

Practical tips

  • Start generous and observe real traffic before tightening.
  • Use dry-run/log-only mode first (limit_req_dry_run on; in Nginx).
  • Different tiers for free and paid plans; document limits publicly.
  • Exempt health checks and internal services.
  • Alert on unusual 429 rates — they may indicate an attack or a broken client.

Rate limiting complements, but doesn’t replace, authentication and abuse detection — see API security best practices and Fail2Ban.

  • APIs
  • Rate Limiting
  • Security