iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Rate Limiting

Defensive rate limiting: where to enforce, fixed window vs token bucket vs sliding window, and the headers + recovery flow that good APIs ship.

OWASP — rate limiting

EXAMPLE
# ===== Why rate limit =====
# 1. Slow / stop credential stuffing + brute force on /login, /reset, /verify
# 2. Cap accidental loops and runaway scripts before they cost real money
# 3. Smooth bursts so a single client cannot starve others
# 4. Compliance with abuse policies on external APIs you depend on

# ===== Where to enforce (in order of preference) =====
# 1. Edge / WAF / CDN -- absorbs hostile bursts before they hit your origin
# 2. Reverse proxy (nginx, Envoy, Traefik) -- per IP / per route
# 3. App middleware -- per user / per API key / per business limit
# Layered limits are normal; each layer protects against a different failure mode.

# ===== Algorithms (mental model) =====
# Fixed window:   N requests per minute, reset on the clock. Easy. Bursty at window boundary.
# Sliding log:    Keep timestamps; count those within the window. Exact, memory-hungry.
# Sliding window: Approximate by blending the previous + current window weight. Cheap + accurate.
# Token bucket:   Refills at R tokens/sec, capped at B. Allows bursts up to B. Smooth long-run.
# Leaky bucket:   Drains at constant rate; queue or drop excess. Steady RPS, predictable downstream.

# Most APIs ship token bucket (burst-friendly) at the API key tier and sliding-window at the IP tier.

# ===== nginx (edge) =====
http {
  # 10 r/s per IP, burst 20, queue + delay none
  limit_req_zone $binary_remote_addr zone=login:10m rate=10r/s;

  server {
    location /login {
      limit_req zone=login burst=20 nodelay;
      proxy_pass http://app;
    }
  }
}

# ===== Express (Node) with rate-limiter-flexible =====
import express from 'express';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';

const redis = new Redis();
const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'rl:login',
  points: 20,         // 20 requests
  duration: 60,       // per 60 seconds
  blockDuration: 600, // block 10 minutes after exhausted
});

const app = express();
app.post('/login', async (req, res, next) => {
  try {
    const r = await limiter.consume(req.ip);
    res.setHeader('X-RateLimit-Limit', limiter.points);
    res.setHeader('X-RateLimit-Remaining', r.remainingPoints);
    res.setHeader('X-RateLimit-Reset', Math.ceil(r.msBeforeNext / 1000));
    next();
  } catch (e) {
    res.setHeader('Retry-After', Math.ceil(e.msBeforeNext / 1000));
    return res.status(429).json({ error: 'too_many_requests' });
  }
});

# ===== Standard headers (RFC 6585 + draft) =====
# X-RateLimit-Limit:     total allowed in window
# X-RateLimit-Remaining: how many left
# X-RateLimit-Reset:     seconds until reset
# Retry-After:           seconds to wait (on 429)
# Newer draft (RateLimit / RateLimit-Policy) lands in some frameworks; pick a convention and document it.

# ===== Per-route limits (rules of thumb) =====
# /login, /reset, /verify    : strict (5-20 / minute / IP)
# /signup                    : strict (3-10 / minute / IP) + CAPTCHA after N
# Auth API key endpoints     : per-key budgets (token bucket, e.g. 100 rps)
# Public read endpoints      : generous + cache aggressively
# Webhook receivers          : per-source key, document retry semantics

# ===== Defensive composition =====
# - IP limit first (cheap to evaluate, stops dumb floods)
# - User/key limit second (catches sophisticated abuse with rotated IPs)
# - Business limit third (e.g. 'no more than 5 orders / minute / customer')

# ===== Recovery and UX =====
# On 429:
#   - Send Retry-After so honest clients back off cleanly
#   - Log the event with route + identifier + algorithm + retry window
#   - Surface a clean error in the UI; do not leak the algorithm

# ===== Backoff on the client =====
# Exponential backoff with jitter:
#   sleep = min(cap, base * 2 ** attempt) + random(0, jitter)
# Without jitter, retries thunder back simultaneously when blocks expire.

# ===== Patterns to internalise =====
# - Layer edge + app limits; they fail differently
# - Token bucket for API keys, sliding window for IPs
# - Always emit Retry-After + RateLimit headers
# - Add per-business limits (per customer, per organisation) in addition to per-IP
# - Test with a load tool BEFORE production traffic finds the gap

# ===== Pitfalls =====
# - IP-only limits when traffic comes through NAT or CGNAT -> blocks legitimate fleets
# - Per-route limits that ignore /api/v1 vs /api/v2 cross-routing -> bypass via path
# - In-memory limiter on a multi-instance app -> per-instance counts; share via Redis
# - Forgetting CAPTCHA / step-up after threshold -> attackers just rotate identities
# - Burning legitimate users on legitimate retries -> tune the burst and the Retry-After

Why it matters

Rate limiting is the cheapest control with the biggest effect on auth surfaces. Layer edge + app, mix token bucket and sliding window per tier, emit Retry-After + RateLimit headers, and add business limits beyond just IPs. Tune once, log everything, and the noisy half of abuse falls off the map.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Limit login / password reset / account creation.
// Add CAPTCHA after N failures. Use sliding-window in Redis or a managed WAF.
Try it Yourself »

Discussion

Loading…