Rate Limiting for Web Applications: What Actually Works
All articles
rate limitingweb securityAPI securityDDoS protectionweb application security

Rate Limiting for Web Applications: What Actually Works

Rate limiting for web applications explained with real algorithms, HTTP status codes, and config examples for Nginx, Express, and Cloudflare.

WebSentry TeamSeptember 7, 20266 min read

Most rate limiting implementations fail in one of two ways: they're either so loose that credential stuffing bots sail right through, or so aggressive that legitimate users get locked out during a traffic spike. Getting it right requires understanding what you're actually protecting against and picking the right algorithm for each scenario — not just dropping a generic middleware package into your stack and calling it done.

Why Rate Limiting Matters Beyond "Stopping DDoS"

Rate limiting is often framed purely as DDoS protection, but that's only one use case. In practice, it protects against:

  • Brute-force login attacks — bots trying thousands of password combinations against your auth endpoint
  • Credential stuffing — attackers testing leaked username/password pairs from other breaches
  • API abuse and scraping — competitors or bots hammering your endpoints for data
  • Resource exhaustion — a single misbehaving client or script overwhelming your database with expensive queries
  • Cost control — if you're paying per API call to a third-party service (OpenAI, Twilio, Stripe), unbounded requests can generate a massive bill

If your only defense is a firewall or CDN-level DDoS filter, you're missing the application-layer abuse that looks like "normal" traffic to network-level tools.

The Main Rate Limiting Algorithms

Before writing any code, decide which algorithm fits your use case. They behave very differently under burst traffic.

1. Fixed Window

Counts requests in a fixed time block (e.g., 100 requests per minute, reset at :00). Simple to implement but has a boundary problem: a client can send 100 requests at 11:59:59 and another 100 at 12:00:01, effectively getting 200 requests in two seconds.

2. Sliding Window Log

Tracks the exact timestamp of every request in a rolling window. Accurate, but memory-heavy at scale since you're storing every timestamp per client.

3. Sliding Window Counter

A hybrid approach — weights the previous window's count based on how much it overlaps with the current window. Good balance of accuracy and performance, and what most production systems (including Cloudflare and AWS) actually use under the hood.

4. Token Bucket

Each client has a bucket that refills at a fixed rate. Requests consume tokens; if the bucket is empty, requests are rejected or queued. This allows short bursts while enforcing a long-term average rate — ideal for APIs where occasional spikes are legitimate.

5. Leaky Bucket

Similar to token bucket but processes requests at a constant outflow rate, smoothing bursts entirely. Good for protecting downstream services (like a database) that need predictable, steady load.

Choosing Limits That Don't Break Real Users

Arbitrary numbers cause more problems than they solve. Base limits on actual usage data:

  1. Pull 30 days of access logs and find the 95th and 99th percentile request rate per user/IP for your busiest legitimate endpoints
  2. Set your limit at 2-3x the 99th percentile — enough headroom for genuine spikes, tight enough to catch abuse
  3. Apply stricter limits to sensitive endpoints (login, password reset, checkout) regardless of what general API traffic looks like
  4. Re-evaluate quarterly — traffic patterns shift as your product grows

A common mistake: applying one global limit across every endpoint. A login form should allow maybe 5 attempts per 15 minutes per IP, while a public search endpoint might reasonably allow 60 requests per minute per user.

Implementation Examples

Nginx (limit_req)

http {
  limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

  server {
    location /login {
      limit_req zone=login burst=3 nodelay;
      proxy_pass http://backend;
    }
  }
}

This limits the login endpoint to 5 requests per minute per IP, allowing a small burst of 3 before rejecting.

Express.js (express-rate-limit)

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  standardHeaders: true,
  message: 'Too many login attempts, try again later.'
});

app.post('/login', loginLimiter, loginHandler);

Cloudflare Rate Limiting Rules

If you're behind Cloudflare, you can enforce limits at the edge before traffic even reaches your origin server — better for both performance and cost. Configure a rule matching your login path with a threshold like "5 requests per 60 seconds" and an action of "Block" or "Managed Challenge."

Return the Right HTTP Status Code

Use 429 Too Many Requests, not a generic 403 or 500. Include a Retry-After header so well-behaved clients know when to try again:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": "rate_limit_exceeded", "retry_after": 60}

This matters for legitimate API consumers building against your service — a clear 429 response lets them implement proper backoff logic instead of retrying blindly or failing silently.

Rate Limiting by Identity, Not Just IP

IP-based limiting alone is easy to bypass with rotating proxies or residential IP pools. Layer in additional identifiers:

  • Authenticated requests: limit by user ID or API key, not just IP
  • Unauthenticated requests: combine IP with device fingerprint or session token
  • Distributed attacks: watch for patterns across many IPs hitting the same endpoint with identical payloads — this needs application-level detection, not just per-IP counters

Common Mistakes That Undermine Rate Limiting

  • Only rate limiting the frontend, not the API directly — attackers skip your UI and hit the API endpoints straight
  • Storing counters in-memory on a single server — this breaks entirely once you scale horizontally; use Redis or a shared store
  • Forgetting about GraphQL — a single GraphQL request can contain dozens of nested queries; count query complexity, not just request count
  • No monitoring on rejected requests — if you're not logging 429s, you have no visibility into whether your limits are too strict, too loose, or under active attack
  • Rate limiting without other security layers — rate limiting doesn't replace proper headers, CSP, or CORS configuration; it's one layer among many

Rate Limiting Is One Piece of the Puzzle

A well-tuned rate limiter stops brute-force and abuse traffic, but it won't catch a missing Strict-Transport-Security header, a misconfigured CORS policy that allows any origin, or cookies missing the Secure flag. These gaps get exploited just as often as the absence of rate limiting itself, and they're frequently overlooked because they don't show up until someone specifically audits for them.

Tools like WebSentry scan for exactly these kinds of gaps — SSL configuration, security headers, CSP, cookie flags, DNS records, and CORS policies — and roll them into a single A–F grade so you can see where your defenses actually stand, not just where you assume they do. Agencies managing client sites often run WebSentry across their whole portfolio to catch configuration drift before a client's security team (or an attacker) finds it first.

If you've locked down rate limiting but haven't checked the rest of your security posture recently, run a free scan at websentry.dev and see what your grade looks like.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.