How do you set up rate limiting and DDoS protection in NGINX?
Quick Answer
NGINX uses the limit_req module with a leaky bucket algorithm to control how fast clients can send requests, usually tracked by IP. For DDoS protection, you combine rate limiting with connection limits (limit_conn), request size caps, timeout tuning, and geo-blocking.
Detailed Answer
Picture a nightclub with a bouncer at the door. The bouncer lets people in at a steady pace -- say ten per minute. If a big crowd shows up at once, some wait in a short line and the rest are turned away. The bouncer also limits how many people from the same group can be inside at the same time. This is exactly how NGINX rate limiting and connection limiting work together: they control the flow of requests so your backend servers don't get overwhelmed.
NGINX rate limiting uses two directives that work as a pair. The limit_req_zone directive goes in the http block and sets up a shared memory zone that tracks request rates. You give it a key (usually $binary_remote_addr for per-IP tracking), a zone name and size, and a rate like 10r/s for ten requests per second. The limit_req directive goes inside a server or location block, points to that zone, and optionally sets a burst value with nodelay or delay. The burst parameter creates a small queue for requests that come in faster than the limit. With nodelay, burst requests are handled right away but anything beyond the burst is rejected. Without nodelay, excess requests are queued and released at the set rate. If you don't set burst at all, any request over the limit is immediately rejected with a 503 (or whatever you set with limit_req_status).
Under the hood, NGINX uses the leaky bucket algorithm. Think of a bucket with a hole in the bottom that drains at a fixed rate (your configured rate). Each request adds water. If the bucket overflows (burst is exceeded), the request is dropped. The shared memory zone stores state for each key -- using $binary_remote_addr takes just 64 bytes per entry, so a 10MB zone can track about 160,000 unique IPs. Connection limiting with limit_conn_zone and limit_conn works similarly but tracks how many connections are open at the same time rather than how fast they arrive. This matters for DDoS protection because one attacker IP could open hundreds of connections at once to exhaust your server's file descriptors.
In production, good DDoS protection layers multiple defenses. Rate limiting handles application-layer (Layer 7) attacks by capping request speed. Connection limiting caps simultaneous connections per IP. You should also set tight client_body_timeout, client_header_timeout, and keepalive_timeout values to quickly close slow connections, which stops Slowloris-style attacks. The limit_req_log_level directive lets you log rate-limited requests at warn level so you can monitor them without flooding your logs. For blocking traffic from known bad IP ranges, the geo module lets you deny entire subnets. For serious DDoS attacks, NGINX works alongside upstream services like Cloudflare, AWS Shield, or hardware appliances that handle network-layer (Layer 3/4) floods before traffic even reaches NGINX.
A common mistake is setting rate limits too tight, which blocks real users -- especially those behind corporate NATs where many people share one IP address. Always start with loose limits and tighten gradually while watching your metrics. Another mistake is applying rate limits globally without exempting health check endpoints, which causes your load balancer's health checks to fail and triggers false downtime alerts. Also, $binary_remote_addr doesn't give you the real client IP when NGINX sits behind another proxy like a CDN. In that case, you need the realip module to extract the actual client IP from X-Forwarded-For before applying any rate limits.