Nginx rate limit uses two directives: limit_req_zone (request rate per IP/key) and limit_conn_zone (concurrent connections per IP/key). The team that uses limit_req for API endpoints has DDoS protection. The team that uses limit_conn for download endpoints has bandwidth protection. The burst parameter allows short spikes; nodelay rejects excess immediately.
Table of contents
- limit_req_zone
- Rate syntax
- Burst and nodelay
- limit_conn
- Combined limits
- Keying by other than IP
- Logging rejections
- Testing
- FAQ
limit_req_zone
Define a rate limit zone:
# 10 MB zone, key by client IP, 10 req/sec average
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
Apply in a location:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
The team that uses limit_req has 10 req/sec per IP with burst tolerance of 20.
Rate syntax
rate=10r/s # 10 requests per second
rate=10r/m # 10 requests per minute
rate=1r/s # 1 request per second (slow)
The team that uses per-second rates for APIs and per-minute for less critical endpoints.
Burst and nodelay
By default, excess requests wait in a queue:
limit_req zone=api burst=20;
Up to 20 excess requests can wait. Without nodelay, requests are spaced out.
With nodelay:
limit_req zone=api burst=20 nodelay;
Up to 20 excess are processed immediately, then the rate limit kicks in. The team that uses nodelay has immediate rejection of excess; the team that omits it has delayed requests.
limit_conn
Limit concurrent connections:
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_conn conn 10;
}
Each IP can have at most 10 concurrent connections. The team that uses limit_conn for download endpoints has bandwidth protection.
Combined limits
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn conn 5;
proxy_pass http://backend;
}
The team that combines limits has both rate and connection protection.
Keying by other than IP
Key by API key, header, etc.:
limit_req_zone $http_x_api_key zone=api_key:10m rate=100r/s;
The team that uses API-key-based limits has per-customer rate limits.
Logging rejections
limit_req_zone ... zone=api:10m rate=10r/s;
limit_req_status 429; # return 429 Too Many Requests
limit_req_log_level warn;
The team that uses 429 (not the default 503) has semantically correct responses. The team that logs rejections has audit trail of who’s hitting limits.
Testing
# Spam requests, observe 429
for i in {1..30}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost/api/; done
The team that tests rate limits has confidence the limit actually triggers.
FAQ
What’s the difference between limit_req and limit_conn?
limit_req limits request rate (requests per second). limit_conn limits concurrent connections. The team that uses both has comprehensive protection.
How much memory does the zone need?
About 16KB per 1000 unique IPs (binary_remote_addr). 10MB zone holds ~600K IPs. The team that sizes zones for their traffic avoids the zone filling up.
What HTTP status does rate-limited requests get?
Default 503 Service Unavailable. Set limit_req_status 429; for 429 Too Many Requests (more semantically correct).
Does rate limit affect the upstream?
Yes - rejected requests don’t reach the backend. The team that uses rate limit has upstream protection during traffic spikes.
Can I rate limit per URL?
Yes with map: map $request_uri $uri_limit { default api; /admin admin; }. The team that has different limits per endpoint uses map.
If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.
Useful related references: