
Your VPS can feel “slow” even when CPU and RAM look fine. The usual culprit is a request flood. Bots hammer /wp-login.php, scrapers walk every URL, or one IP opens hundreds of connections.
This Nginx rate limiting tutorial shows how to add practical limits. You’ll protect WordPress and API endpoints without punishing real visitors.
This guide targets normal hosting conditions. Think multiple sites, mixed traffic, and changes you may need to roll back fast.
If you want the quickest route to a clean Nginx baseline on infrastructure you control, start with a HostMyCode VPS. You own the stack and the logs.
What you’ll build (and what you won’t)
- Per-IP request rate limits for sensitive paths like WordPress login and XML-RPC.
- Connection limits to stop slowloris-style behavior and overly chatty clients.
- Separate “zones” so one bad endpoint doesn’t throttle your whole site.
- Clear diagnostics in access logs so you can prove what got limited and why.
You don’t need a CDN, paid WAF, or a full bot-management suite to get real protection.
We also won’t touch HTTP/3/QUIC here. This is traffic control, not protocol tuning.
Prerequisites and safe rollout plan
This guide assumes Nginx is already serving your site(s) on Ubuntu 24.04/24.10, Debian 12/13, AlmaLinux 9/10, or Rocky Linux 9/10.
If Nginx is fronting Apache (common on hosting VPS setups), these controls still belong at the Nginx layer.
Before you change anything
- Confirm you can reload Nginx safely:
sudo nginx -tthensudo systemctl reload nginx. - Make sure you won’t lock yourself out over SSH. If your sessions drop, fix that first using this internal guide: VPS SSH timeout troubleshooting.
- Take a snapshot/backup if this is production. One bad config line can take the whole vhost down.
Find your Nginx config layout
Common paths:
- Ubuntu/Debian:
/etc/nginx/nginx.conf, sites in/etc/nginx/sites-available/and/etc/nginx/sites-enabled/ - AlmaLinux/Rocky:
/etc/nginx/nginx.conf, vhosts often in/etc/nginx/conf.d/
Nginx rate limiting tutorial: the 3 directives you’ll use
Nginx request throttling follows a consistent pattern:
limit_req_zonecreates a shared-memory “zone” keyed by something (usually the client IP).limit_reqapplies that zone to a server or location, with a rate and a burst allowance.limit_req_status(plus logging) makes throttling visible in your logs.
Connection limiting uses the same idea:
limit_conn_zonedefines a zone.limit_connenforces it in a server/location.
Step 1 — Add rate-limit zones globally (http context)
Edit /etc/nginx/nginx.conf and define the zones inside the http { ... } block.
Put them near the top of http so you can find them quickly later.
# /etc/nginx/nginx.conf
http {
# ... existing config ...
# Key by binary address (more memory-efficient than $remote_addr)
limit_req_zone $binary_remote_addr zone=login_limit:20m rate=5r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc_limit:20m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api_limit:30m rate=60r/m;
# Connection limits (helps with abusive keep-alive / slow clients)
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
# Optional: surface rate-limited events with a clear status
limit_req_status 429;
# ... existing config ...
}
How to choose zone sizes
Each zone uses shared memory. In practice, 20–30MB per zone works well on most VPS plans. It also covers many unique IPs.
If you serve high traffic with lots of unique visitors, increase the zone sizes.
Quick check after the change:
sudo nginx -t && sudo systemctl reload nginx
Step 2 — Apply limits to WordPress login and admin endpoints
Attack traffic tends to cluster around a few WordPress URLs. The goal is friction.
Slow brute force attempts down. Still let real users retry passwords or refresh.
In your site’s server block (for example /etc/nginx/sites-available/example.com), add targeted location rules.
server {
server_name example.com www.example.com;
# Baseline connection limit for the whole vhost
limit_conn conn_per_ip 25;
# WordPress login: low rate, small burst
location = /wp-login.php {
limit_req zone=login_limit burst=10 nodelay;
include fastcgi_params;
# ... your php-fpm config ...
}
# wp-admin often pulls assets; don’t over-tighten
location ^~ /wp-admin/ {
limit_req zone=api_limit burst=30;
# ... existing config (try_files, proxy, fastcgi, etc.) ...
}
# XML-RPC is commonly abused; many sites can disable entirely
location = /xmlrpc.php {
limit_req zone=xmlrpc_limit burst=5 nodelay;
include fastcgi_params;
# ... your php-fpm config ...
}
# ... rest of server ...
}
Disable XML-RPC if you don’t need it
If you don’t use Jetpack, the WordPress mobile app, or legacy integrations, you can often block XML-RPC outright.
That removes a common target and cuts avoidable PHP work.
location = /xmlrpc.php {
return 444;
}
If you want a gentler rollout, keep rate limiting first.
If the logs show nonstop abuse, switch to blocking.
If you’re using LiteSpeed instead of Nginx for WordPress performance, the tuning points are different. This internal guide covers that stack: LiteSpeed Cache setup guide.
Step 3 — Rate limit API routes without punishing normal browsing
Modern themes and plugins can generate a lot of AJAX and REST traffic. Most of it is legitimate.
It’s also an easy place for abuse. Limit the endpoints, not the whole site.
WooCommerce / WordPress AJAX
location = /wp-admin/admin-ajax.php {
# Allows bursts for cart updates, filters, etc.
limit_req zone=api_limit burst=60;
include fastcgi_params;
# ... php-fpm config ...
}
WordPress REST API
location ^~ /wp-json/ {
# Most sites can tolerate 60 req/min per IP with bursts
limit_req zone=api_limit burst=120;
# ... proxy_pass or fastcgi ...
}
If you operate a public API that third parties rely on, set limits based on real consumption.
A hard 60 req/min may be too low. In those cases, use API keys and separate server blocks, or whitelist trusted IP ranges.
Step 4 — Preserve real client IPs behind a reverse proxy or load balancer
If Nginx sits behind Cloudflare, HAProxy, or another proxy, $remote_addr becomes the proxy’s IP.
If you rate limit on that, you effectively rate limit everyone as one user.
Enable the real IP module and trust only your proxy sources. Example for Cloudflare (their IPv4/IPv6 ranges can change; verify against Cloudflare’s current list in 2026):
# /etc/nginx/conf.d/realip.conf (or inside http {})
real_ip_header CF-Connecting-IP;
# Trust Cloudflare ranges (example only; verify current ranges)
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;
# ... add remaining IPv4/IPv6 ranges ...
real_ip_recursive on;
Once real IP is in place, your limits key off the visitor’s address again, not the proxy.
Step 5 — Log rate-limited events so you can tune safely
If you can’t see what’s being throttled, you’ll end up guessing.
Add a log format that makes 429s easy to find and ties them to a request path.
# inside http {}
log_format main_with_limits '$remote_addr - $host "$request" $status '
'rt=$request_time ua="$http_user_agent"';
access_log /var/log/nginx/access.log main_with_limits;
Then you can pull recent throttles quickly:
sudo awk '$9==429 {print}' /var/log/nginx/access.log | tail -n 20
Quick diagnostic: are you limiting real users?
- If 429s spike during business hours and user agents look like normal browsers, your rate is too strict or the burst is too small.
- If 429s are mostly odd user agents repeatedly hitting
/wp-login.php, the limits are doing their job.
Step 6 — Add a small allowlist for admin IPs (optional, but practical)
If you have a fixed office IP (or a VPN exit IP), you can exempt it from limits.
Keep this tight. Don’t whitelist broad ranges “just in case.”
Create a map:
# /etc/nginx/conf.d/limit-allowlist.conf
map $binary_remote_addr $limit_key {
default $binary_remote_addr;
# Replace with your static admin IP (example)
203.0.113.10 "";
}
Then update your zones to key off $limit_key instead of $binary_remote_addr:
limit_req_zone $limit_key zone=login_limit:20m rate=5r/m;
When $limit_key is empty, Nginx doesn’t track that request in the zone.
In practice, that bypasses the limit for the allowlisted IP.
Step 7 — Test the limits without guessing
Run a small test from a workstation. Avoid testing from the server itself.
This matters even more if you’re behind a proxy, where client IP handling differs. Here are two quick options.
cURL loop for wp-login.php
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php
done
Once you exceed the burst, you should see 429.
ApacheBench (ab) for a single URL
ab -n 200 -c 10 https://example.com/wp-login.php
Keep concurrency modest on production, especially during peak hours.
You’re validating behavior, not stress-testing the box.
Common pitfalls (the stuff that causes surprise outages)
- Rate limiting the whole site: applying
limit_reqatserver {}level often throttles image/CSS bursts and makes pages feel broken. - Forgetting real IP headers: if you’re behind a proxy, everybody shares one IP and hits limits instantly.
- Too-small bursts on login: browsers can trigger multiple requests due to redirects, cookies, and assets. A burst of 10 is usually safe.
- Not accounting for monitoring: uptime checks can look like “bots.” Add your monitoring IPs to the allowlist if they get 429s.
- No rollback plan: always keep a clean config version. If you’re unsure, stage changes in a separate include file.
How this fits into a hosting-friendly security baseline
Nginx limits are one layer. They work best with SSH and firewall hygiene, plus sensible account security.
- If SSH is exposed, fix that first. A straightforward production firewall helps immediately: iptables/nftables production firewall tutorial.
- If you run cPanel/WHM, pair rate limits with control-panel protections like cPHulk and 2FA. Start here: cPanel brute force protection tutorial.
Checklist: a safe “first deployment” for most VPS websites
- Global zones: login 5 req/min burst 10; API 60 req/min burst 120.
- Endpoints limited:
/wp-login.php,/xmlrpc.php,/wp-admin/admin-ajax.php,/wp-json/. - Connection cap:
limit_conn25 per IP at server level. - Real IP configured if behind a proxy.
- Logging verified: you can query for 429 responses in logs.
- Rollback ready: config changes in a dedicated include file you can remove fast.
When to move from “DIY limits” to managed hosting help
If you’re spending more time chasing abusive traffic than running your business, simplify.
On busy WordPress servers (or multi-site boxes), support also helps when limits collide with caching, PHP-FPM pools, or upstream proxies.
For that setup, managed VPS hosting is a solid middle ground.
You keep root-level control, but you’re not alone when a traffic spike shows up at 2 a.m.
If you’re implementing Nginx rate limits on production sites, you’ll get the best results on a VPS where you control Nginx, logs, and firewall rules. Spin up a HostMyCode VPS for full control, or choose managed VPS hosting if you want help tuning limits without impacting customers.
FAQ
What’s a good starting rate for WordPress login throttling?
For most sites, 5 requests per minute per IP with a burst of 10 is a good baseline. It slows brute force attempts while still allowing normal password retries.
Should I return 429 or 503 for rate-limited traffic?
Use 429. It clearly means “too many requests,” and it keeps your logs easier to interpret. Reserve 503 for upstream failures and maintenance windows.
Will rate limiting break WooCommerce carts or checkouts?
Not if you limit only the sensitive endpoints and allow bursts. Avoid strict limits at the server level; scope them to admin-ajax.php, wp-json, and login routes.
I’m behind Cloudflare and everyone gets throttled. Why?
Nginx is probably treating Cloudflare IPs as the client IP. Configure real_ip_header and set_real_ip_from so Nginx keys limits to the actual visitor address.
Summary: keep the site fast by limiting the right URLs
Rate limiting works best when you keep it surgical.
Put strict limits on login and abuse-prone endpoints. Leave normal browsing alone.
Make sure Nginx sees real client IPs.
Done right, you cut PHP load, smooth traffic spikes, and eliminate a lot of “mystery slowness” that isn’t a hosting issue at all.
If you want a stable platform for these controls—plus predictable performance for WordPress, APIs, and multi-site hosting—run it on a HostMyCode VPS or step up to dedicated servers when you need consistent headroom under heavy traffic.