VPS Tutorials
What to Do When a VPS Website Is Hit by a CC Attack (2026): Cloudflare WAF, Nginx Rate Limiting, and Origin Protection Emergency Checklist
When a VPS website suddenly shows high CPU, saturated bandwidth, and an Nginx access.log flooded by the same batch of IPs or paths, it is not necessarily a normal traffic surge — it may well be a CC attack, malicious crawler, or API abuse. This article follows the real emergency order: first confirm from logs whether it is an HTTP-layer attack, then use Cloudflare WAF/Rate Limiting to block at the edge, use Nginx limit_req and limit_conn to protect the origin as a fallback, and explain how to avoid harming search engines and normal users.

When a website suddenly slows down, don't rush to restart the VPS.
If you see CPU spiking, bandwidth saturated, PHP-FPM or the backend application timing out everywhere, and the same path being hammered in the Nginx access.log, this looks more like a CC attack / malicious crawler / API abuse than a normal "traffic increase."
The most troublesome part of this kind of problem is that the requests all look like normal HTTP requests, unlike a traditional DDoS that is just a flood of traffic. It may hammer the homepage, search pages, login pages, or APIs, and it may also use a bunch of scattered IPs to slowly saturate your database and PHP-FPM.
This article follows the real handling order: first confirm the attack, then let Cloudflare block a layer at the edge, and finally use Nginx to do fallback rate limiting at the origin.
The most intuitive signals are usually these:
- The VPS CPU or load average suddenly rises;
access.logrequest volume is abnormal, several times to dozens of times higher than usual;- A certain path is requested repeatedly, for example
/wp-login.php,/xmlrpc.php,/search,/api/*; - Nginx can still respond, but the backend starts returning 502, 504;
- Cloudflare Analytics or the provider's traffic graph shows abnormal spikes;
- Normal users access slowly, but static resources may still be fine.
First look at the IPs with the highest recent request volume:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
Then look at the most heavily hammered paths:
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
If your website is behind Cloudflare, the Nginx logs may be full of Cloudflare node IPs, in which case you need to configure the real visitor IP first, otherwise you will misjudge.
The common approach in Nginx is to use CF-Connecting-IP:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
Cloudflare IP ranges get updated, so in production it is recommended to check the official list regularly. After configuring, first run:
sudo nginx -t
sudo systemctl reload nginx
If you are not yet familiar with locating issues in logs, you can read this first:
If the attack traffic already passes through Cloudflare, handle it at Cloudflare first. The reason is simple: blocking requests at the edge avoids consuming your VPS CPU, memory, bandwidth, and database connections.
For a temporary emergency, you can do three things first:
- Turn on the managed rules in Security / WAF;
- Add a Custom Rule for the abnormal paths;
- Add a Rate Limiting Rule for heavy paths such as login, search, and API.
For example, if the attack is concentrated on /wp-login.php, you can first add a stricter rule:
- Matching path:
/wp-login.php - Counting dimension: IP
- Threshold: 5-10 times per minute
- Action: Managed Challenge or Block
- Ban duration: 10 minutes to 1 hour
If an API is being hammered, such as /api/search, the threshold can be looser than for the login page:
- 60-120 times per minute;
- Return 429 or Managed Challenge after exceeding;
- Make exceptions for logged-in users, internal monitoring, or trusted IPs.
Don't block the whole site right away. That is how many small sites get hurt: search engines, payment callbacks, monitoring probes, and normal mobile users all get blocked together — the attack is mitigated, but so is the business.
Cloudflare's official Rate Limiting documentation also notes: rate limiting does not precisely allow a fixed number of requests, and there may be a few seconds of delay between counting and blocking. So the origin still needs fallback configuration.
Nginx's limit_req uses a "leaky bucket"-like approach: requests can burst briefly, but the long-term rate cannot exceed the threshold you set.
First define the rate-limiting zones in the http block, usually placed in /etc/nginx/nginx.conf or /etc/nginx/conf.d/limits.conf:
limit_req_zone $binary_remote_addr zone=site_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr_limit:10m;
The meaning is roughly:
- Normal pages: 10 requests per second per IP;
- Login/sensitive APIs: 1 request per second per IP;
10mshared memory is enough to track a large number of IPs;$binary_remote_addruses less memory than a plain IP string.
Then apply it in the site configuration:
server {
server_name example.com;
limit_req zone=site_limit burst=30 nodelay;
limit_conn addr_limit 20;
limit_req_status 429;
limit_conn_status 429;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /wp-login.php {
limit_req zone=login_limit burst=5 nodelay;
limit_req_status 429;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location = /xmlrpc.php {
deny all;
}
}
Several details matter here:
burst=30gives normal browsers brief burst room when loading a page;nodelaymeans burst requests are not queued and are handled directly according to the rule once exceeded;limit_req_status 429is clearer than the default 503, meaning too many requests;- WordPress's
xmlrpc.phpshould be turned off directly if not used; - APIs, login, and search pages should be rate-limited separately; don't use one threshold for the whole site.
After modifying, always test first:
sudo nginx -t
sudo systemctl reload nginx
If you recently changed the Nginx configuration and encounter a reload failure, you can read this first:
Rate limiting is not better the stricter it is.
I have seen a small site limit the whole site to 1r/s, and as a result the homepage had a dozen CSS, JS, and image resources, so users got 429 as soon as they opened the page. The attack was not fully blocked, but normal users were shut out first.
After going live, first check the Nginx error log:
sudo tail -f /var/log/nginx/error.log | grep "limiting requests"
If you see something like:
limiting requests, excess: 12.450 by zone "site_limit", client: 203.0.113.10
it means this IP is triggering rate limiting.
Then count by status code:
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
If 429s suddenly become numerous, keep looking at the paths:
sudo awk '$9 == 429 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
If the 429s are concentrated on /wp-login.php, /xmlrpc.php, /api/search, that is usually a good thing; if the homepage, static resources, and health checks also get a large number of 429s, the rules are too coarse.
Many people relax after putting Cloudflare in front, but if the origin IP has already leaked, attackers can directly access your VPS IP and bypass Cloudflare.
The typical signals of this situation are:
- Not many requests are visible on Cloudflare;
- But VPS bandwidth and Nginx logs are very high;
- A large number of non-Cloudflare source IPs appear in Nginx;
- Directly accessing
http://你的源站IPcan open the website.
At minimum, do two things.
First, the Nginx default site should not directly serve the real website:
server {
listen 80 default_server;
server_name _;
return 444;
}
Second, the firewall should only allow Cloudflare IPs to access 80/443. Be careful with this step: first confirm you can log in via SSH and that the Cloudflare IP ranges are complete, otherwise you may cut off your own website.
If the origin IP has been targeted for a long time, the cleanest approach is usually:
- Open a new VPS or change the public IP;
- Fill only the new origin IP into Cloudflare DNS;
- Restrict the firewall so 80/443 only accepts Cloudflare;
- Do not leak the new IP in email headers, Git repositories, historical DNS, or monitoring pages.
If the attack causes Cloudflare 521/522, you can refer to:
If the attack comes from a small number of fixed IPs, blocking IPs is useful:
sudo ufw deny from 203.0.113.10
Or temporarily use iptables:
sudo iptables -I INPUT -s 203.0.113.10 -j DROP
But if the attack comes from hundreds or thousands of IPs, manually blocking IPs is basically pointless. You will make the rule table messier and messier, and the CPU still has to process a large number of connections.
In this case, it is better to use:
- Cloudflare WAF / Rate Limiting;
- Nginx
limit_reqandlimit_conn; - Fail2Ban to automatically ban IPs that continuously trigger rate limiting;
- The provider's high-defense protection or upstream DDoS protection.
If you already suspect the machine has been compromised rather than simply flooded with requests, first follow the intrusion handling process:
If the website is being hammered, I would handle it in this order:
- First look at the Nginx access.log to confirm high-frequency IPs and high-frequency paths;
- If going through Cloudflare, first enable WAF / Managed Challenge / Rate Limiting;
- Focus protection on login, search, API, and
xmlrpc.php; - Add
limit_req,limit_conn, and the 429 status code in Nginx; - Check the real IP configuration to avoid Nginx only seeing Cloudflare node IPs;
- Restrict the firewall so the origin only accepts Cloudflare access;
- Observe changes in 429, 403, 502, 504 to avoid harming normal users;
- After the attack ends, review: whether the origin IP leaked, which paths are most expensive, and whether caching or architecture splitting is needed.
Don't treat rate limiting as a panacea. For truly large-scale volumetric DDoS, Nginx will be saturated before it can handle it, and you must rely on Cloudflare, the provider's high-defense protection, or upstream scrubbing. Nginx rate limiting is better suited to blocking small and medium-scale CC, malicious crawlers, API abuse, and login brute force — HTTP-layer requests.
It can. Especially if you set the whole-site rate limit too low, or don't leave enough room for Cloudflare Verified Bots, Googlebot, and Bingbot. A more stable approach is to prioritize limiting login, search, APIs, and dynamic pages, and not to come down hard directly on static resources and normal pages.
For most ordinary small sites, it is enough most of the time. The free plan provides CDN, basic DDoS protection, WAF rules, and some Bot protection capabilities. But more granular Rate Limiting, Bot Management, and complex counting dimensions usually require a higher plan.
For a small number of fixed IPs, yes. For distributed attacks, manual blocking is not recommended; use a combination of Cloudflare, Nginx rate limiting, Fail2Ban, and provider protection.
Rate limiting is better suited to returning 429, meaning Too Many Requests. Clear malicious rules, blacklists, empty User-Agent, and attack tool signatures can return 403.
When a VPS website is hit by a CC attack, the two extremes are the most dangerous: only knowing how to restart services, and blocking the whole site right away.
A more stable approach is layered handling: Cloudflare blocks the bulk at the edge, Nginx uses limit_req and limit_conn as a fallback at the origin, the firewall prevents the origin IP from being bypassed, and logs continuously observe harm to normal users and attack paths.
If you only remember one thing: first use logs to confirm the attack path, then rate-limit the most expensive paths, and don't blindly rate-limit the whole site.