Blog
4 min read

Nginx Rate Limiting: limit_req, Bursts and Protecting Your Login Page

How to rate-limit requests in Nginx with limit_req_zone and limit_req: choosing a key and rate, what burst and nodelay really do, returning 429 instead of 503, separate limits for login and API routes, getting the real client IP behind Cloudflare, connection limits, and testing it.

Rate limiting stops one client from sending too many requests: bots brute-forcing your login, scrapers hammering an expensive endpoint, or someone burning through your AI API budget. Nginx can do it before requests ever reach your app. (What Is Rate Limiting?)

The two directives

limit_req_zone (in the http block) defines a limit: what to count by, where to store counters, and the rate.

limit_req (in a server or location) applies it.

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

    server {
        location / {
            limit_req zone=general burst=20 nodelay;
            proxy_pass http://127.0.0.1:3000;
        }

        location = /api/login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://127.0.0.1:3000;
        }
    }
}
  • $binary_remote_addr — limit per client IP (the binary form uses less memory than $remote_addr).
  • zone=login:10m — a named shared-memory zone; 10 MB holds roughly 160,000 IPs.
  • rate=5r/m — five requests per minute. Use r/s for per-second.
  • limit_req_status 429 — Nginx's default rejection status is 503, which looks like an outage to monitoring and search engines. 429 is the correct "too many requests". (429 Too Many Requests)

burst and nodelay, demystified

Nginx enforces the rate smoothly: 10r/s really means "one request every 100 ms". Without a burst, two requests 50 ms apart — normal for a page loading several resources — would get the second one rejected.

  • burst=20 — allow up to 20 extra requests to queue instead of being rejected.
  • without nodelay — queued requests are released at the configured rate, so the client sees them slowed down.
  • with nodelay — queued requests are served immediately, but still use up burst slots, which refill at the rate. Excess beyond the burst is rejected.

burst=N nodelay is what most people want: normal users never notice, abusive clients get 429s.

There's also delay=N for a middle ground: the first N excess requests go through immediately and the rest are slowed.

Different limits for different routes

Be strict where abuse is costly, generous elsewhere:

Route Suggested starting point
Login, signup, password reset A few per minute per IP
Endpoints that call paid AI APIs Low, per IP — and per user in your app
General API Tens per second with a burst
Static assets Usually none

(Brute-Force Attacks, Stop Bots Running Up Your AI Bill)

Behind Cloudflare or a load balancer: get the real IP

If Nginx sits behind Cloudflare, every request comes from a Cloudflare IP — so per-IP limiting would throttle everyone together. Use the real-IP module to trust the forwarded header from known proxy ranges:

set_real_ip_from 173.245.48.0/20;   # add every Cloudflare range
# ...
real_ip_header CF-Connecting-IP;

Only trust headers from proxy IPs you list; otherwise anyone can fake their IP. (Cloudflare Proxied vs DNS Only, CIDR Notation Explained)

Limiting connections too

limit_conn caps simultaneous connections, useful against slow clients holding connections open:

limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
    limit_conn perip 20;
}

Test it

for i in $(seq 1 15); do curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/api/login -X POST; done

You should see 200s (or 401s) followed by 429s. Rejections are logged to the error log. Add limit_req_dry_run on; first to log what would be limited without blocking anyone. (Load Testing Your App)

Nginx limits are a first line, not the only one

IP-based limits don't stop distributed attacks or abuse from logged-in accounts. Add per-user limits in your app (often with Redis), CAPTCHAs on sensitive forms, and account lockouts. (Implement Rate Limiting, Add a CAPTCHA to Your Forms)

The summary

  • limit_req_zone defines the limit; limit_req applies it per location.
  • Use burst=N nodelay so real users aren't affected, and limit_req_status 429.
  • Behind a proxy, set the real client IP first — or you'll limit everyone at once.

EasySpawn puts a managed proxy with HTTPS in front of every app, and Claude Code can add per-route and per-user rate limits where your app needs them. See how it works or join the waitlist.

Related: Nginx Reverse Proxy Configuration · fail2ban · What Is a DDoS Attack? · Nginx Location Blocks and try_files

Keep reading