Not all attacks are alike. A volumetric attack tries to saturate the network link: a traffic jam on the motorway. By-Hoster protection handles that upstream, with nothing required from you.
An application-layer attack, known as layer 7, is different. The traffic stays modest, but every request is expensive: a search, a login page, a shopping cart. To the network these look like ordinary visitors. Only at your web server does the nature of the traffic become visible, so that is where you have to act.
How do I find out what is actually hitting my server?
Change nothing until you know what is arriving. A setting chosen at random usually blocks your real visitors without inconveniencing the attacker.
The busiest IP addresses:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
The most requested URLs:
awk -F'"' '{print $2}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
Recent traffic, to judge intensity:
tail -n 100000 /var/log/nginx/access.log | awk '{print $4}' | uniq -c | tail -20
You are looking for a pattern: a handful of very talkative addresses, a single URL being hammered, an identical user agent everywhere. That pattern dictates the rule, not the other way round.
Also check what is actually saturating:
uptime
systemctl status php8.3-fpm --no-pager
Often it is not Nginx that buckles but PHP or the database behind it.
How do I rate-limit requests per address in Nginx?
Nginx can count requests per address and slow down those that exceed a threshold. Zones are declared in the http block (/etc/nginx/nginx.conf):
http {
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
}
Then applied in the site block:
server {
limit_req zone=req_per_ip burst=20 nodelay;
limit_conn conn_per_ip 20;
}
What this means in practice:
| Setting | Effect |
|---|---|
rate=10r/s |
10 requests per second per address in steady state |
burst=20 |
tolerates a spike of 20 requests above the rate |
nodelay |
serves the spike immediately instead of spreading it out |
limit_conn 20 |
at most 20 simultaneous connections per address |
A 10m zone tracks roughly 160,000 addresses, which is ample.
burst without nodelay queues requests: the site becomes slow instead of refusing. With nodelay, a normal visitor loading a page and its twenty assets passes without slowdown, while a persistent bot receives a 503.
Protect genuinely expensive URLs more tightly rather than the whole site:
location = /wp-login.php {
limit_req zone=req_per_ip burst=3 nodelay;
}
How do I test the Nginx configuration before applying it?
With nginx -t, which validates the syntax without applying anything, followed by a hot reload rather than a restart.
nginx -t
This validates the syntax without interrupting anything. It must answer syntax is ok then test is successful. Only then:
systemctl reload nginx
reload applies the configuration without dropping current connections, unlike restart. In the middle of an attack, that difference matters.
Watch what is being refused:
grep "limiting requests" /var/log/nginx/error.log | tail -20
If your own addresses or your customers' start appearing, the limit is too low. Raise it.
How do I serve the same page less often?
The best way to absorb an attack is often to stop recomputing the page for every request. A page cached for 60 seconds costs almost nothing, however many times it is asked for.
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=site:50m inactive=10m;
}
location / {
proxy_cache site;
proxy_cache_valid 200 60s;
proxy_cache_use_stale error timeout updating;
}
proxy_cache_use_stale is the line that saves you: if the backend stops answering, Nginx keeps serving the last known version instead of an error.
On WordPress, PrestaShop or Laravel, an application cache plugin achieves the same with fewer side effects.
How do I ban repeat offenders with fail2ban?
Limiting slows down; banning stops. fail2ban reads the logs and adds a firewall rule against addresses that trip the limit too often.
apt install fail2ban
In /etc/fail2ban/jail.local:
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 3600
systemctl restart fail2ban
fail2ban-client status nginx-limit-req
Read aloud, the rule says: an address that trips the limit 10 times within 60 seconds is blocked for an hour.
Which settings should I avoid against an application-layer attack?
These settings circulate widely and do more harm than good.
| Avoid | Why |
|---|---|
Blocking curl and wget by user agent |
An attacker changes agent in one line. You break your monitoring, your payment webhooks and your backup scripts. |
client_max_body_size 1k |
Every image upload, every longer form, every CMS update fails. |
allow on one subnet then deny all |
You close the site to the whole internet, visitors included. Keep that pattern for admin URLs. |
| Banning entire countries reflexively | Many false positives, and attackers rent machines everywhere. |
| Disabling logs "for performance" | You lose the only way to understand what is happening to you. |
A simple rule: any measure that blocks wider than the observed pattern will be paid for in lost customers.
When should I open a ticket with you?
When the problem is beyond what you can fix on the machine: a saturated link, a long attack, or thousands of addresses.
Open a ticket if:
- the machine stays unreachable despite these settings;
- traffic saturates the network link before it even reaches Nginx;
- the attack persists and comes from thousands of distinct addresses.
Attach the log extracts from step 1 and the time window concerned. That is what lets us act upstream, on the network, where you cannot.