Rate-limit at the edge before the request costs you anything
The claim The place to reject abusive traffic is the cheapest place in your stack to reject it, and that is the reverse proxy at the edge — before the request reaches your applicat...
The claim
The place to reject abusive traffic is the cheapest place in your stack to reject it, and that is the reverse proxy at the edge — before the request reaches your application, opens a database connection, or consumes a worker. Teams routinely build rate limiting into the application, where every rejected request has already cost nearly as much as an accepted one. Rejecting at the edge means a flood of ten thousand requests a second costs you almost nothing, because Nginx says no before your expensive code ever runs.
Why the layer matters more than the algorithm
A request that reaches your application has already consumed the expensive resources: a worker process is occupied, a database connection may be checked out, memory is allocated. Rejecting it there protects you from bad logic but not from load — ten thousand rejected requests still tie up ten thousand workers' worth of attention. Rejecting the same request at the reverse proxy costs a hash lookup and a counter increment. The same defence at a different layer is the difference between an attack you shrug off and an attack that exhausts your connection pool while you are busy rejecting it.
The configuration
Nginx implements a leaky-bucket limiter in a few lines. Define zones by what you are protecting:
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location / {
limit_req zone=general burst=60 nodelay;
}
location = /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
}
}
Two parameters carry the design. rate is the sustained allowance; burst is how many requests may queue above it before rejection, which absorbs legitimate bursts like a page that fires several requests on load. nodelay serves the burst immediately rather than smoothing it out, which is what you want for interactive traffic. The 10m zone size holds roughly 160,000 distinct client addresses, which is plenty for a single host.
Set different limits for different endpoints
A single global limit is the wrong model, because your endpoints have wildly different costs and abuse profiles. The login route deserves a strict limit measured in requests per minute, because that is where credential-stuffing attacks land and no legitimate user logs in ten times a second. A search endpoint that hits the database deserves a tighter limit than a static asset. A public API deserves per-key limits, not per-IP, because a legitimate integration behind a single corporate address should not be throttled as if it were one abusive user. The zones above are the mechanism; matching each route's limit to its real cost and risk is the actual work.
The header that makes limits usable
A rate limit that rejects without explanation generates support tickets from legitimate clients who cannot tell throttling from a bug. Return the standard headers so well-behaved clients can back off correctly:
RateLimit-Limit: 30
RateLimit-Remaining: 12
RateLimit-Reset: 18
Retry-After: 18
Retry-After on a 429 response tells a client exactly how long to wait, and any competent API consumer will honour it. This turns rate limiting from an adversarial black box into a contract the client can cooperate with, which dramatically reduces the support burden of running limits at all.
The trap: limiting by IP behind a proxy
If your Nginx sits behind a CDN or load balancer, $binary_remote_addr is the address of that intermediary, not the client — so every request appears to come from a handful of proxy IPs, and you either rate-limit all your users as one or disable the limit in confusion. You must read the real client address from the forwarded header, and you must only trust that header from your known proxies:
set_real_ip_from 10.0.0.0/8; # your CDN/LB ranges only
real_ip_header X-Forwarded-For;
Trusting X-Forwarded-For from arbitrary sources is its own vulnerability, because an attacker can then forge the header to evade the limit or frame another address. Trust it only from the specific ranges your infrastructure uses.
Where the edge is not enough
Edge rate limiting handles volume and simple abuse, but some limits are business rules that only the application knows: five failed payment attempts per account per day, one password reset email per address per fifteen minutes, a per-tenant quota on an expensive report. These are correctly enforced in the application, because they depend on identity and state the proxy cannot see. The right architecture is both layers: the edge sheds volume cheaply so the application never sees the flood, and the application enforces the rules that require knowing who the user is. Start at the edge, because it is the cheapest and catches the most, then add the application-level rules where a real business limit needs them.