--- title: "How to Configure Rate Limiting in Nginx on Ubuntu 24.04" description: "Limit requests per client in Nginx on Ubuntu 24.04 with limit_req and limit_conn: burst handling, stricter login limits, IP allowlists, 429 responses and tests." category: "Web Servers" language: en source: https://cubepath.com/docs/web-servers/rate-limiting-configuration-in-nginx updated: 2026-09-25 --- # How to Configure Rate Limiting in Nginx on Ubuntu 24.04 Rate limiting caps how many requests a single client can make in a period of time. It protects login forms from brute force attacks, keeps APIs usable when one client misbehaves and absorbs small floods before they reach your application. Nginx implements it with the `limit_req` module, which is built into the Ubuntu package. In this tutorial you will configure a general per-IP limit, a stricter limit for a login endpoint, an allowlist for trusted addresses and a proper `429` response, then test that the limits work. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has `sudo` privileges. - Nginx installed (`sudo apt install nginx`) with a site configured in `/etc/nginx/sites-available/your_domain`. The examples use `your_domain` as the server name. - The `apache2-utils` package on the machine you test from, for the `ab` benchmarking tool (optional). ## Step 1 - Understanding how limit_req works Nginx rate limiting uses the **leaky bucket** algorithm and needs two directives: - `limit_req_zone` (in the `http` context) defines a shared memory **zone**, the **key** that identifies a client and the **rate** allowed per key. - `limit_req` (in `http`, `server` or `location`) applies a zone to requests, optionally with a **burst** queue. A rate of `10r/s` does not mean "10 requests at any moment within a second". Nginx spreads it evenly: one request every 100 ms. Without `burst`, a second request arriving 50 ms after the first is rejected. The `burst` parameter lets a client exceed the rate briefly by queuing extra requests, and `nodelay` serves the queued requests immediately instead of spacing them out, while still counting them against the limit. | Configuration | Behavior for a client sending 30 requests at once at 10r/s | |---|---| | no `burst` | 1 served, 29 rejected | | `burst=20` | 21 served, spaced 100 ms apart (last one after 2 s), 9 rejected | | `burst=20 nodelay` | 21 served immediately, 9 rejected, slots free up at 10 per second | For websites and APIs, `burst` with `nodelay` is almost always what you want: browsers load many assets in parallel, and delaying them makes the page feel slow. ## Step 2 - Defining the rate limit zones Create a file in `/etc/nginx/conf.d/`, which Ubuntu's `nginx.conf` includes inside the `http` block: ```bash sudo nano /etc/nginx/conf.d/rate-limit.conf ``` ```nginx # General limit: 10 requests per second per client IP limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s; # Strict limit for login and password reset: 5 requests per minute per IP limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # Reply with 429 Too Many Requests instead of the default 503 limit_req_status 429; limit_req_log_level warn; ``` The key `$binary_remote_addr` is the client IP in binary form, which uses less memory than the text `$remote_addr`. A 1 MB zone stores about 16,000 client states, so `10m` holds about 160,000 IP addresses. When a zone is full, Nginx removes the oldest entries; if it still can't make room, it rejects the request. `limit_req_status 429` returns the status code that tells clients they are being rate limited, and `limit_req_log_level warn` writes rejected requests to the error log at `warn` level (delayed ones are logged one level lower). Test the configuration: ```bash sudo nginx -t ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Defining a zone does not limit anything yet; the next step applies it. ## Step 3 - Applying the limits to your site Open your site configuration: ```bash sudo nano /etc/nginx/sites-available/your_domain ``` Apply the general limit to the whole `server` block, and the strict limit to the login endpoint. Adjust `location = /login` to the path your application uses (for example `/wp-login.php` for WordPress): ```nginx server { listen 80; server_name your_domain; root /var/www/your_domain; limit_req zone=perip burst=20 nodelay; location = /login { limit_req zone=login burst=5 nodelay; # Keep passing the request to your application here, for example: # proxy_pass http://127.0.0.1:8000; } location / { try_files $uri $uri/ =404; } } ``` A `limit_req` directive in a `location` replaces the ones inherited from the `server` level, so `/login` is only limited by the `login` zone. If you want both limits on the same location, list both directives inside it; Nginx applies all of them and the strictest one wins. Reload Nginx: ```bash sudo nginx -t && sudo systemctl reload nginx ``` ## Step 4 - Testing the limits Send 30 requests as fast as possible and count the status codes: ```bash for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}\n" http://your_domain/; done | sort | uniq -c ``` ```output 21 200 9 429 ``` The first request plus the burst of 20 are served, and the rest are rejected. Wait a few seconds and run the loop again: the bucket has drained and requests are accepted. Test the login limit the same way: ```bash for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" http://your_domain/login; done | sort | uniq -c ``` ```output 6 200 4 429 ``` Only the first request and the burst of 5 get through. If no application answers on `/login` yet, the accepted requests return `404` instead of `200`; what matters is that the last four return `429`. Because the rate is 5 per minute, a new slot opens every 12 seconds. For a load test with concurrency, `ab` reports how many responses were not `2xx`: ```bash ab -n 200 -c 20 http://your_domain/ | grep -E 'Complete requests|Non-2xx' ``` ```output Complete requests: 200 Non-2xx responses: 179 ``` Each rejected request is also logged in the error log: ```bash sudo tail -n 3 /var/log/nginx/error.log ``` ```output 2026/09/25 10:42:17 [warn] 2318#2318: *845 limiting requests, excess: 20.300 by zone "perip", client: 203.0.113.25, server: your_domain, request: "GET / HTTP/1.1", host: "your_domain" ``` > Tip: Before enforcing limits on a busy production site, add `limit_req_dry_run on;` next to `limit_req`. Nginx then logs which requests would be rejected without rejecting them, so you can tune the rate and burst on real traffic first. ## Step 5 - Excluding trusted clients Monitoring systems, your office network or internal services should not be limited. Nginx does not account requests whose key is an empty string, so you can use `geo` and `map` to give trusted clients an empty key. Replace the general zone in `/etc/nginx/conf.d/rate-limit.conf`: ```bash sudo nano /etc/nginx/conf.d/rate-limit.conf ``` ```nginx geo $rate_limited { default 1; 127.0.0.1 0; 10.0.0.0/8 0; 203.0.113.10 0; } map $rate_limited $rate_limit_key { 0 ""; 1 $binary_remote_addr; } limit_req_zone $rate_limit_key zone=perip:10m rate=10r/s; ``` Replace `203.0.113.10` and the private range with your own trusted addresses. Keep the `login` zone and the other directives from Step 2 as they were. Reload Nginx and repeat the test loop from a trusted address: all 30 requests now return `200`. ## Step 6 - Limiting concurrent connections `limit_req` controls the request rate, but a client can also hold many slow connections open, for example while downloading large files. The `limit_conn` module limits the number of simultaneous connections per key. Add the zone to `/etc/nginx/conf.d/rate-limit.conf`: ```nginx limit_conn_zone $binary_remote_addr zone=connperip:10m; limit_conn_status 429; ``` Then apply it where needed, for example to a downloads location in your site: ```nginx location /downloads/ { limit_conn connperip 5; } ``` Each IP can now keep at most five downloads running at the same time. Test and reload as before. ## Step 7 - Returning a useful 429 response By default Nginx returns its generic HTML error page. For an API, return JSON and a `Retry-After` header so well-behaved clients know when to try again. Add this to the `server` block of your site: ```nginx error_page 429 = @ratelimited; location @ratelimited { default_type application/json; add_header Retry-After 1 always; return 429 '{"detail": "Too many requests, please slow down."}'; } ``` Reload Nginx, trigger the limit with the loop from Step 4 and look at one rejected response: ```bash for i in $(seq 1 25); do curl -s -o /dev/null http://your_domain/; done; curl -i http://your_domain/ ``` ```output HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 1 {"detail": "Too many requests, please slow down."} ``` ## Step 8 - Rate limiting behind a proxy or CDN If Nginx sits behind a load balancer or a CDN such as Cloudflare, every request arrives from the proxy's IP address, and all visitors share a single limit. Use the `realip` module, which is included in Ubuntu's Nginx, to restore the client address before the limits are evaluated. Add this to the `http` context, for example in `/etc/nginx/conf.d/real-ip.conf`: ```nginx set_real_ip_from 10.0.0.5; real_ip_header X-Forwarded-For; real_ip_recursive on; ``` Replace `10.0.0.5` with the address or range of your proxy and use one `set_real_ip_from` line per range. Only list proxies you control or trust: any client that can reach Nginx directly from a listed address can set its own IP. For Cloudflare, list its published IP ranges and use `real_ip_header CF-Connecting-IP;`. After reloading, the error log should show real client addresses in `client:` rather than the proxy's address. ## Step 9 - Banning repeat offenders with Fail2Ban Rate limiting rejects excess requests but does not stop a client from trying again. Fail2Ban includes a filter for the "limiting requests" messages in the Nginx error log, and can ban clients that keep hitting the limit at the firewall level: ```bash sudo apt install fail2ban sudo nano /etc/fail2ban/jail.d/nginx-limit-req.local ``` ```ini [nginx-limit-req] enabled = true backend = auto maxretry = 10 findtime = 10m bantime = 1h ``` Restart Fail2Ban and check the jail: ```bash sudo systemctl restart fail2ban sudo fail2ban-client status nginx-limit-req ``` Remember to exclude your own address with an `ignoreip = 127.0.0.1/8 203.0.113.10` line in the jail if you test from it. ## Troubleshooting **Limits never trigger.** Check that the `limit_req` directive is in the `server` or `location` that actually handles the request (`sudo nginx -T | grep -n limit_req` shows the full effective configuration), and that the client key is not empty because of the allowlist. **All visitors are limited together.** Nginx sees the IP address of a proxy or CDN. Configure the `realip` module as in Step 8. **Legitimate users get `429` when loading pages.** The burst is too small for the number of parallel requests a page triggers (images, scripts, API calls). Increase `burst`, keep `nodelay`, or exclude static assets from the general limit by setting `limit_req` only in the locations that serve dynamic content. **`nginx -t` fails with `zero size shared memory zone`.** A `limit_req` directive references a zone name that is not defined by any `limit_req_zone`. Check the spelling in both places. ## Conclusion Nginx now limits every client to a steady request rate with room for bursts, applies a much stricter limit to the login endpoint, skips trusted addresses, limits concurrent downloads and answers with a clear `429` response. As next steps, watch the error log for a few days and tune rate and burst to your real traffic, add separate zones for expensive API endpoints, and combine rate limiting with the rest of your Nginx hardening such as security headers and modern TLS settings.