A sudden run of 429 Too Many Requests can mean your limit is catching an abusive client, or that a shared office or mobile network is being throttled with it. If 502 and 504 responses are climbing, the application behind NGINX may already be under pressure. First identify which layer is saturated, preserve access and error logs, and avoid applying a blanket per-IP rule. Then contain expensive HTTP paths with a targeted NGINX policy, after confirming the active server block and the identity used as its limit key.
NGINX rate limiting can slow or reject requests that reach the web server. It can't restore an uplink or provider network that's already saturated. Treat it as an application-layer control within a broader defense, not as a substitute for upstream mitigation.
Table of Contents
- Recognize DDoS Symptoms and Apply the First Fix
- Prepare NGINX and Establish a Safe Baseline
- Configure Rate and Connection Limits Safely
- Filter Requests and Integrate Fail2ban
- Choose Caching CDN and Upstream Protection
- Test Monitoring and Roll Back the Protection
- Maintain DDoS Protection After Deployment
Recognize DDoS Symptoms and Apply the First Fix
Operators often first see a sudden rise in 429, 499, 502, or 504 responses, longer response times, or unstable connections to an upstream application. On the host, rising ESTAB or SYN-RECV socket counts and file-descriptor exhaustion can point to connection pressure. None of these symptoms alone proves that a DDoS attack is underway. A deployment issue, a failing backend, or a legitimate traffic surge can look similar.
Start by recording the evidence before changing limits. Check NGINX's active configuration and recent logs, compare error rates across affected paths, and inspect socket state and host resource pressure. Don't delete logs, restart services unnecessarily, or introduce a broad deny rule while you're still determining whether traffic reaches NGINX at all.
| Observed symptom | Likely exhaustion point | First diagnostic action |
|---|---|---|
429 responses concentrated on one path | A request limit or application policy | Check the matching location block and access logs |
499 responses with rising latency | Clients are closing slow requests | Compare upstream response times and NGINX error logs |
502 or 504 responses | Upstream health, capacity, or connection pressure | Inspect upstream errors and backend health |
Rising ESTAB or SYN-RECV counts | Socket or connection pressure | Run ss -s and inspect worker and file-descriptor limits |
| High bandwidth with little request visibility | Network link or upstream saturation | Check provider and edge telemetry before tuning NGINX |
If the requests are reaching a costly endpoint, contain that path rather than imposing the same limit across the site. NGINX's ngx_http_limit_req_module uses a leaky-bucket algorithm and can delay requests, then reject excess requests after configured burst capacity is exhausted. Its documentation describes rate limiting as a way to help prevent DDoS attacks and protect proxied upstream servers from overload. NGINX request rate limiting
First-response rule: Keep the site's working paths available while you gather evidence. A blanket rule may suppress attack traffic, but it may also lock out legitimate users behind a shared address.
If you can't tell whether the bottleneck is at the host, edge, or provider, use an incident checklist such as ARPHost's DDoS attack detection guide. Escalate upstream if the network link, load balancer, or TLS terminator is saturated before requests can be evaluated by NGINX.
Prepare NGINX and Establish a Safe Baseline
Before editing the edge, confirm what NGINX is running and how its service is configured. You'll need root or sudo access, the active configuration tree, access to current logs, a way to check upstream health, and a maintenance window for reloads. Commands below target a Linux host using systemd; package and service names can differ by distribution.
Capture the current build and configuration paths:
nginx -v
nginx -V 2>&1
nginx -T
nginx -V reports build options and compiled modules. nginx -T tests and prints the loaded configuration, including included files. Treat its output as sensitive if it contains credentials or internal details, and don't paste it into a public ticket.
Inspect capacity and current connections before changing worker settings:
nginx -T 2>&1 | grep -E 'worker_processes|worker_connections|worker_rlimit_nofile'
ulimit -n
ss -s
The official core documentation lists a default of 512 simultaneous connections per worker, and clarifies that the count includes connections to proxied upstream servers. The effective ceiling can also be constrained by the process file-descriptor limit. NGINX core module documentation Raising worker_connections alone won't overcome a lower service file-descriptor limit, and neither setting prevents an attack that saturates the network link before packets reach the host.
Back up the configuration before editing. On Debian or Ubuntu, a common configuration root is /etc/nginx; verify yours with nginx -T first.
sudo cp -a /etc/nginx "/etc/nginx.backup-$(date +%Y%m%d%H%M%S)"
sudo nginx -t
| Check | Command | Information captured |
|---|---|---|
| NGINX build | nginx -V 2>&1 | Version and compiled modules |
| Loaded configuration | nginx -T | Active directives and included files |
| Worker and file limits | nginx -T and ulimit -n | Configured connection ceiling and shell limit |
| Socket state | ss -s | Current connection summary |
| Baseline logs | tail -n 100 /var/log/nginx/access.log | Recent requests and response codes |
A directive can be valid in principle and still fail on a particular build if its module isn't available. A rate limit also needs a shared-memory zone declared in the http context before a server or location can use it. Keep the pre-change backup until the modified configuration has passed syntax checks and the service has returned to its normal state.
For every edit, run sudo nginx -t before reloading. If the check passes, use sudo systemctl reload nginx; if the check fails, don't reload. To roll back, restore the saved configuration, run the syntax check again, and reload only after it passes.
Configure Rate and Connection Limits Safely
A limit is only as useful as its key. Limiting by $binary_remote_addr is simple, but it assumes each address represents one client. That assumption breaks behind corporate NAT, mobile carrier gateways, and other shared IPv4 networks. On IPv6, the choice of address granularity also affects whether a user's changing address or a group of addresses is treated as one identity. A strict per-address policy can block legitimate customers while attackers spread requests across many addresses.
The configuration below demonstrates the mechanism, not a universal production threshold. NGINX's documentation gives 10 requests per second as an example rate, corresponding operationally to about one request every 100 milliseconds because the module tracks requests at millisecond granularity. The rate and burst values below are starting values to evaluate against your own endpoint traffic, not a promise that they're safe for every application.

Declare zones inside the http block. The rate zone tracks request activity per key. The connection zone provides a separate per-key connection control.
http {
limit_req_zone $binary_remote_addr zone=per_ip_requests:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=per_ip_connections:10m;
log_format limits '$remote_addr $status $request_method $uri '
'request_time=$request_time upstream_time=$upstream_response_time';
server {
listen 80;
server_name example.invalid;
access_log /var/log/nginx/access.log limits;
location = /login {
limit_req zone=per_ip_requests burst=10;
limit_req_status 429;
limit_conn per_ip_connections 10;
limit_conn_status 429;
proxy_pass
}
location / {
proxy_pass
}
}
}
The burst setting allows a short queue of requests above the sustained rate. By default, NGINX can delay excess requests within the configured burst; adding nodelay changes that behavior by passing requests in the burst immediately, while still enforcing the limit afterward. Once the permitted burst is exhausted, excess requests are rejected. A delayed response can smooth a brief spike, but it also consumes time and may not be appropriate for latency-sensitive endpoints. Rejection protects the upstream sooner, but can affect legitimate bursts.
Choose a key that matches the traffic
Use separate policies for anonymous pages, login, search, checkout, API requests, webhooks, and authenticated users. A search endpoint may do expensive work that ordinary static pages don't. A webhook sender may retry after a slow response, so an indiscriminate policy can amplify delivery failures.
| Traffic class | Recommended identity source | Control to tune | Main false-positive risk |
|---|---|---|---|
| Anonymous pages | Client address, only when the address is trustworthy | Request rate and burst | Shared networks and carrier NAT |
| Login | Client address plus application-level account controls | Request rate and rejection behavior | Shared offices and mobile gateways |
| Authenticated API | A validated account or token identity where available | Per-identity quota and endpoint cost | Many users sharing one credential |
| Webhooks | Known sender identity and application verification | Rate and retry handling | Legitimate retries grouped as abuse |
| HTTP/2 or HTTP/3 clients | Request identity, not a simple connection count | Request rate plus endpoint policy | Multiplexed requests misread as one-to-one connections |
If NGINX sits behind a proxy or CDN, $remote_addr may identify that proxy instead of the visitor. Configure real client address handling only for proxy addresses you trust. Never trust arbitrary forwarded headers from the public internet, because a client could forge them and evade an address-based limit.
http {
set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
limit_req_zone $binary_remote_addr zone=per_ip_requests:10m rate=10r/s;
}
Replace the example proxy range with the actual trusted proxy ranges supplied by your provider. Don't copy the documentation range as a live setting. Confirm the resulting $remote_addr in logs before enforcing limits. For identity-aware limits, use a validated application identity only when the application can reliably set it; an untrusted request header isn't an identity.
HTTP/2 and HTTP/3 can multiplex requests over a connection, so a connection count isn't a request count. Don't assume that limit_conn alone controls request floods. Likewise, don't set limits so aggressively that one user's parallel page assets or API calls are treated as abuse.
This is what the policy looks like operationally: one noisy tenant or endpoint can drive upstream pressure while other sites on shared hardware remain healthy. A per-IP rule can make the symptoms disappear for one address and still miss a distributed flood. Watch the affected endpoint, upstream latency, and client complaints together.
For a practical walkthrough of the request-to-upstream path, this video provides additional context:
Validate the completed file and observe the behavior before adjusting it:
sudo nginx -t
sudo systemctl reload nginx
sudo tail -f /var/log/nginx/access.log
If legitimate requests start receiving 429, reduce enforcement or scope the rule to the expensive location, then retest. Roll back by restoring the saved configuration and reloading after nginx -t succeeds.
Filter Requests and Integrate Fail2ban
Rate limits control request frequency. Request filtering can reject methods or paths your application doesn't serve, but a deny rule must be narrow enough not to catch legitimate clients. Encoded paths, application-specific routes, and clients that rotate addresses all make simplistic pattern matching risky.
If the application only accepts a known set of methods, reject other methods in the relevant location rather than across unrelated virtual hosts:
location /api/ {
limit_except GET POST {
deny all;
}
proxy_pass
}
Check the application's method requirements before deploying this. For example, a client may use PUT or DELETE legitimately. A broad regular expression against every URI can also block valid encoded paths or API routes. Prefer exact locations for known unwanted paths, and review access logs before adding a pattern.
Fail2ban can apply temporary firewall bans to repeated matching behavior, but it's not a substitute for upstream filtering. Use a dedicated filter and jail so you can disable only the new rule if it catches legitimate users. The example below matches repeated 404 responses. Confirm that your access log format places the status code where the expression expects it.
Create /etc/fail2ban/filter.d/nginx-repeated-404.conf:
[Definition]
failregex = ^<HOST> - .* "(GET|POST|HEAD) .*" 404
ignoreregex =
Create /etc/fail2ban/jail.d/nginx-repeated-404.local:
[nginx-repeated-404]
enabled = true
filter = nginx-repeated-404
logpath = /var/log/nginx/access.log
backend = auto
findtime = 10m
maxretry = 20
bantime = 10m
action = nftables-multiport[name=nginx-repeated-404, port="http,https", protocol=tcp]
The threshold and durations are sample jail settings, not a universal policy. Verify the installed Fail2ban version and available action files on your distribution. Confirm the log path, then test the filter against a representative log entry before enabling the jail.
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-repeated-404.conf
sudo fail2ban-client -t
sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-repeated-404
sudo tail -n 50 /var/log/fail2ban.log
To remove a test ban, identify the address in jail status and unban it:
sudo fail2ban-client set nginx-repeated-404 unbanip 192.0.2.10
Replace the example address with the actual test address. For rollback, disable only nginx-repeated-404 in its jail file and reload Fail2ban. If the action created firewall rules that remain, inspect the active ruleset and remove only the rules associated with that jail. For host-level filtering and rule management, see ARPHost's Linux firewall guidance.
| Behavior | NGINX response | Fail2ban action | Rollback |
|---|---|---|---|
| Unsupported method | Reject in the scoped location | None required | Remove the method restriction |
Repeated matching 404 responses | Log each response | Temporarily ban the matching address | Disable the dedicated jail and unban test clients |
| Valid route that resembles a blocked pattern | May reject a legitimate request | Could extend the false positive into a ban | Narrow the match and remove the ban |
| Requests spread across many addresses | Per-key limits may not catch the aggregate | Per-address bans may have limited effect | Escalate to edge or upstream controls |
Choose Caching CDN and Upstream Protection
NGINX should sit at the application layer of a defense, not at the front of every decision. Cloudflare reported mitigating 6.9 million DDoS attacks in the fourth quarter of 2024; 49% were Layer 3 or Layer 4 attacks and 51% were HTTP DDoS attacks. Those categories matter because NGINX can regulate HTTP requests that reach it, but it can't stop network congestion that prevents traffic from reaching the server. Cloudflare's fourth-quarter 2024 DDoS report
Choose a mitigation layer based on where the pressure occurs and what traffic it can safely handle:
| Layer | Best-fit traffic | Primary protection | Escalate when |
|---|---|---|---|
| NGINX request limits | HTTP requests reaching the origin | Endpoint-specific rate and connection controls | Distributed requests remain above safe application thresholds |
| Reverse-proxy cache | Repeated, cacheable content | Reduces repeated origin work | Attack traffic bypasses cache or targets dynamic paths |
| CDN filtering | Public web traffic and distributed HTTP requests | Edge request filtering and caching | Network or provider capacity is still under pressure |
| Provider filtering or scrubbing | Network floods before the host | Filters traffic upstream of the origin | Provider cannot preserve service or route clean traffic |
| Load balancer controls | Connection and TLS pressure at the edge | Connection handling and TLS termination | The balancer itself reaches its capacity |
| Application authentication | Login, account, and mutation endpoints | Restricts costly actions to validated identities | Abuse uses valid identities or overwhelms shared services |
A cache helps only when responses can be safely reused. Static assets and public pages are often better cache candidates than login, search, checkout, personalized pages, or API mutations. Caching a response that varies by user without correct cache controls can expose private content. Set cache behavior deliberately, and verify that the origin receives fewer requests for the content you intended to cache.
Escalate beyond NGINX when bandwidth rises while application logs show little corresponding request activity, or when the load balancer and TLS terminator are struggling before requests reach the origin. For HTTP floods, compare request patterns and upstream latency, then coordinate edge filtering with endpoint-specific NGINX controls. A load-balancing design also affects where connections and TLS work accumulate; see ARPHost's load balancing setup guide.
The trade-off is straightforward: controls closer to the origin can be precise about application paths, but they act after traffic has traversed the network and any upstream infrastructure. Edge and provider controls can discard traffic earlier, but need enough visibility to avoid blocking legitimate users.
Test Monitoring and Roll Back the Protection
A successful nginx -t proves syntax, not resilience. Test the policy in a staging environment or against a controlled endpoint first. Don't generate disruptive traffic against a production service to simulate an attack.
Use a client you control to request the protected path repeatedly, then inspect response codes and timing:
sudo nginx -t
curl -i
sudo tail -f /var/log/nginx/access.log
ss -s
Replace the example hostname with your test endpoint. Watch NGINX access and error logs, upstream latency, CPU, memory, bandwidth, socket counts, file descriptors, and TLS or worker pressure. A rising 429 count can show that a policy is working, but also check for legitimate-user complaints and failed application transactions.
| Verification target | Test or command | Healthy result | Rollback or escalation |
|---|---|---|---|
| Configuration syntax | sudo nginx -t | Test succeeds | Restore backup if invalid |
| Request policy | curl -i and access-log review | Expected requests pass or receive the intended response | Narrow or remove the affected location policy |
| Socket pressure | ss -s | Counts remain consistent with service behavior | Check connection sources and host capacity |
| Worker and file limits | nginx -T and ulimit -n | Effective limits support expected proxy and client connections | Adjust service limits only after measuring demand |
| Upstream health | Application health check and NGINX error log | Backend remains responsive | Investigate backend capacity or connectivity |
| Network saturation | Host and provider bandwidth telemetry | Traffic reaches the host without link pressure | Escalate to provider or upstream filtering |
On shared infrastructure, a faulty tenant identity rule can create a real availability problem without crashing NGINX. 502 and 504 responses may rise as upstream work backs up, while one site's 429 responses reflect a false positive rather than a successful defense.
Practice rollback before an incident. Restore the backup, run sudo nginx -t, reload only if it passes, and confirm that health checks and normal requests recover. If the limit module is missing, remove unsupported directives or install a build that includes the required module. If clients appear as the proxy address, correct trusted proxy handling before applying per-address rules. If Fail2ban bans persist, inspect the jail's firewall action and remove only its rules. If CDN behavior differs from origin behavior, check cache status and ensure the origin is receiving the expected request headers.
Maintain DDoS Protection After Deployment
Assign an owner to the NGINX policy and record why each location uses its chosen identity and limit. Review normal traffic before changing thresholds, periodically test Fail2ban, and retain configuration backups that can be restored and validated. Keep escalation contacts for the application owner, network provider, CDN, and load balancer operator.
| Recurring task | Owner | Evidence to retain | Escalation trigger |
|---|---|---|---|
| Review limits against traffic patterns | NGINX operator | Configuration and access-log review | Legitimate clients receive repeated rejections |
| Test bans and removal | Security or systems operator | Jail status and test result | Bans persist after the jail is disabled |
| Verify backups and rollback | Service owner | Backup and syntax-check result | Restore fails or service health does not recover |
| Review network and upstream signals | Network and application operators | Bandwidth, socket, and upstream observations | Link, TLS, or backend capacity is exhausted |
In multi-tenant operations, a quiet NGINX process doesn't prove the service is healthy; an upstream can be failing while requests continue to arrive. NGINX is useful for HTTP controls after traffic reaches the server. Bandwidth saturation, connection exhaustion, and TLS pressure need upstream filtering, edge capacity, and a clear escalation path.
For teams that need help operating the full stack around NGINX, ARPHost provides managed IT and infrastructure services alongside VPS, bare metal, colocation, and private cloud options. Review ARPHost, LLC to discuss where application controls end and managed or upstream protection should begin.

Leave a Reply
You must be logged in to post a comment.