Back to tutorials
Tutorial

Nginx rate limiting tutorial (2026): Stop bots, protect WordPress login, and prevent API abuse on a VPS

Nginx rate limiting tutorial for 2026: configure limit_req/limit_conn to block bots and protect WordPress & APIs on your VPS.

By Anurag Singh
Updated on Jun 03, 2026
Category: Tutorial
Share article
Nginx rate limiting tutorial (2026): Stop bots, protect WordPress login, and prevent API abuse on a VPS

Most “traffic spikes” that take down small VPS sites aren’t real visitors. They’re brute-force login bursts, aggressive crawlers, and cheap layer‑7 floods. These attacks chew through PHP workers long before CPU hits 100%.

This Nginx rate limiting tutorial shows how to cap abusive request patterns at the edge. The goal is simple. Keep WordPress logins responsive, keep APIs working, and stop your origin from cooking itself under load.

You’ll build a practical, production-ready setup using limit_req (requests/second) and limit_conn (concurrent connections). You’ll also add a few targeted location rules.

Examples assume Ubuntu 24.04/26.04 LTS-style layouts and Nginx 1.24+. That’s a common packaged baseline on many distributions in 2026.

If you run Debian, AlmaLinux, or Rocky Linux, keep the logic and adjust the paths.

What you’ll achieve (and when to use it)

Rate limiting is the right tool when the server looks “up,” but pages stall during bursts. You’ll usually notice it as:

  • WordPress /wp-login.php crawling (10–30 seconds) during an attack while the homepage still loads.
  • XML-RPC or REST API endpoints getting hammered until PHP-FPM hits max children.
  • Noisy bots hitting the same URLs over and over, burning bandwidth and I/O.

This won’t fix slow queries or a badly sized PHP-FPM pool. It will stop a lot of junk traffic early, before it saturates your upstream workers.

Prerequisites: a clean Nginx baseline on a VPS

You’ll need shell access and a working Nginx site to edit. If you’re still picking infrastructure, a small project can start on a VPS and scale up as traffic grows.

HostMyCode’s HostMyCode VPS plans fit this tutorial well because you get root access and predictable resources for Nginx + PHP-FPM tuning.

First, confirm Nginx is installed. Then find where your config is coming from:

sudo nginx -v
sudo nginx -T | head -n 40
ls -la /etc/nginx/

Common layouts:

  • Ubuntu/Debian: /etc/nginx/nginx.conf + /etc/nginx/sites-available/, /etc/nginx/sites-enabled/
  • RHEL/AlmaLinux/Rocky: /etc/nginx/nginx.conf + /etc/nginx/conf.d/*.conf

Nginx rate limiting tutorial: define zones (the part most people get wrong)

Nginx rate limiting relies on shared-memory “zones.” Zones track request keys, usually the client IP.

Define zones in the http context, not inside a server block.

On Ubuntu/Debian, the http context is typically in /etc/nginx/nginx.conf.

Edit your main config:

sudo nano /etc/nginx/nginx.conf

Inside http { ... }, add zones like these:

# 1) General per-IP request rate (good for most sites)
limit_req_zone $binary_remote_addr zone=perip_rps:20m rate=10r/s;

# 2) Login-focused stricter zone (protects wp-login and admin)
limit_req_zone $binary_remote_addr zone=login_rps:10m rate=1r/s;

# 3) API-focused zone (a little higher than login, still controlled)
limit_req_zone $binary_remote_addr zone=api_rps:20m rate=5r/s;

# 4) Connection limiting zone (concurrent connections per IP)
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;

Why $binary_remote_addr? It’s more memory-efficient than the plain-text IP. With mixed IPv4/IPv6 traffic, it also keeps zone storage predictable.

Don’t undersize zones. If a zone fills up, Nginx can’t track keys consistently. When that happens, limiting gets weird fast.

If you’re behind a proxy/CDN: rate limit on the real client IP, not your load balancer’s address.

That means configuring real_ip_header and trusted proxy IP ranges first. Do that groundwork, then come back and apply the limits.

Apply safe defaults at the server level (then tighten only where needed)

Start with gentle limits across the site. That way, normal browsing doesn’t get punished.

Then clamp down on the handful of endpoints attackers actually abuse.

Add the following inside the relevant server { ... } block for your site (or in a shared include used by several sites).

Open your site config. Example:

sudo nano /etc/nginx/sites-available/example.com

Add these lines near the top of the server block:

# Allow short bursts, then enforce 10 requests/sec
limit_req zone=perip_rps burst=40 nodelay;

# Cap concurrent connections per IP
limit_conn perip_conn 20;

# Return a clear status for rate limited requests
limit_req_status 429;
limit_conn_status 429;

How to read this: a client can spike briefly (page HTML plus assets). Sustained abuse starts getting 429s.

The connection cap helps when a client opens too many keep-alive sockets. It also helps with slow-drip connection hogging.

Validate and reload:

sudo nginx -t
sudo systemctl reload nginx

Protect WordPress hot spots: wp-login, wp-admin, xmlrpc.php

Most WordPress attacks concentrate on a tiny set of URLs. Rate limit those hard.

Keep the rest of the site comfortable for real people.

Add these location blocks inside your WordPress server block:

# 1) wp-login.php: very strict
location = /wp-login.php {
  limit_req zone=login_rps burst=5 nodelay;
  limit_conn perip_conn 5;

  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

# 2) wp-admin: strict, but allow admin assets to load
location ^~ /wp-admin/ {
  limit_req zone=login_rps burst=10 nodelay;
  limit_conn perip_conn 10;

  try_files $uri $uri/ /index.php?$args;
}

# 3) xmlrpc.php: often abused; choose one option
# Option A: rate limit it hard
location = /xmlrpc.php {
  limit_req zone=login_rps burst=2 nodelay;
  limit_conn perip_conn 2;

  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

# Option B: block it entirely if you don't use it
# location = /xmlrpc.php { return 403; }

Adjust the PHP-FPM socket path for your installed version (php8.2-fpm.sock, php8.3-fpm.sock, etc.). On many 2026 WordPress stacks, PHP 8.3 is common.

Some run 8.4 depending on compatibility.

If WordPress runs on Apache behind Nginx, or you’re using a control panel, the FastCGI include will differ. Keep the rate limiting lines as-is.

Adapt the upstream handler to your setup.

If you want a broader security baseline alongside rate limiting, pair this with firewall and SSH hygiene. HostMyCode’s walkthrough is a solid companion: VPS hardening for Ubuntu hosting in 2026.

Limit abusive API traffic without breaking real integrations

APIs (and WordPress REST endpoints) attract automated traffic. One bot can generate thousands of PHP hits per minute.

Give API routes their own limits. They’re usually looser than login limits, but still controlled.

Add:

# WordPress REST API
location ^~ /wp-json/ {
  limit_req zone=api_rps burst=30 nodelay;
  limit_conn perip_conn 10;

  try_files $uri $uri/ /index.php?$args;
}

# Example: custom API prefix
location ^~ /api/ {
  limit_req zone=api_rps burst=50 nodelay;
  limit_conn perip_conn 20;

  proxy_pass http://127.0.0.1:3000;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Tip: if you have authenticated API clients, you can create a separate zone keyed off an API token header instead of IP.

That gets tricky quickly.

IP-based controls are a good first pass while you measure real traffic patterns.

Whitelist your own office IP (and monitoring) the right way

Locking yourself out is a classic first-time mistake. You can exempt known-good sources using geo + a conditional key.

This approach keeps protections on for everyone else.

In http {}, add a whitelist map. Replace the example IPs:

geo $is_whitelisted {
  default 0;
  203.0.113.10 1;   # office
  198.51.100.22 1;  # VPN
}

map $is_whitelisted $limit_key {
  1 "";
  0 $binary_remote_addr;
}

# Re-define zones using $limit_key
limit_req_zone $limit_key zone=perip_rps:20m rate=10r/s;
limit_req_zone $limit_key zone=login_rps:10m rate=1r/s;
limit_req_zone $limit_key zone=api_rps:20m rate=5r/s;
limit_conn_zone $limit_key zone=perip_conn:10m;

When $limit_key is an empty string, Nginx won’t track it for limiting. That effectively exempts the whitelisted source.

It’s cleaner than sprinkling if statements through a dozen location blocks.

Make the 429 responses useful: minimal logging and a clear page

If you log every limited request, your logs can become the problem during an attack. Aim for visibility without writing a million lines per minute.

Create a dedicated log format for throttling events. In the http block:

log_format limited '$remote_addr - $host "$request" $status '
                   'rt=$request_time ua="$http_user_agent"';

Then inside your server block, add:

# Only log 429 responses to a separate file
map $status $log_limited {
  default 0;
  429 1;
}

access_log /var/log/nginx/limited.log limited if=$log_limited;

If your Nginx build rejects map inside server, move the map to http (some builds enforce this). Keep only the access_log line in the server block.

For user experience, serve a simple 429 page:

error_page 429 /429.html;
location = /429.html {
  root /var/www/errors;
  internal;
}

Create the file:

sudo mkdir -p /var/www/errors
sudo nano /var/www/errors/429.html

Keep it short and plain. Tell real users to retry in a few seconds.

Quick diagnostics: prove it works before you need it

Test from your own machine. Start with a harmless endpoint, then hit a protected one.

1) Confirm you see 429 under load (requires curl):

for i in {1..80}; do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php; done | sort | uniq -c

Once you exceed the burst, you should see a mix of 200/302 and 429.

2) Watch the Nginx error log for limit messages:

sudo tail -f /var/log/nginx/error.log

Nginx typically logs limiting requests when limit_req triggers. If you see nothing, check directive placement.

Also confirm you reloaded after editing.

3) If you run UFW: don’t expect it to handle layer‑7 rate limiting. Use UFW for ports and basic filtering. Use Nginx for HTTP behavior.

For a clean baseline, HostMyCode’s UFW setup tutorial for hosting pairs well with this guide.

Common pitfalls (and fixes) on VPS hosting

  • Problem: Everyone gets rate limited behind a CDN/load balancer.
    Fix: configure real client IP handling, then rate limit by the corrected IP.
  • Problem: Legit users hit 429 while loading image-heavy pages.
    Fix: increase burst (e.g., 60–100) but keep the sustained rate moderate.
  • Problem: Attackers rotate IPs; per-IP limits don’t help enough.
    Fix: add WAF/CDN rules, bot filtering, or application-level challenges. Rate limiting still reduces per-IP damage.
  • Problem: You block yourself during deployments or uptime checks.
    Fix: whitelist your office/VPN and monitoring IPs using the geo/map pattern above.
  • Problem: Your server runs out of file descriptors during a connection flood.
    Fix: keep limit_conn in place and check worker_connections and OS limits (ulimit -n).

Optional: make limits more surgical with user-agent and path rules

Some teams throttle “bad citizen” crawlers more aggressively. Treat user-agent matching as a convenience, not a security boundary.

It’s easy to spoof.

Still, for noisy crawlers, this can cut wasted work.

Example approach using map in http:

map $http_user_agent $ua_bucket {
  default 0;
  ~*(MJ12bot|AhrefsBot|SemrushBot) 1;
}

map $ua_bucket $ua_limit_key {
  1 $binary_remote_addr;
  0 "";
}

limit_req_zone $ua_limit_key zone=crawler_rps:10m rate=1r/s;

Apply it only to expensive pages (search endpoints, dynamic listing pages), not to static assets:

location = /search {
  limit_req zone=crawler_rps burst=5 nodelay;
  try_files $uri $uri/ /index.php?$args;
}

Production checklist: rate limiting you can live with

  • Zones live in http {}, not in server {}.
  • Start with gentle global limits; tighten only on login/admin/API endpoints.
  • Use burst to allow normal page loads without 429 spikes.
  • Set limit_req_status 429 so your monitoring can detect throttling.
  • Log 429s separately to avoid log floods during attacks.
  • If you use a proxy/CDN, fix real client IP handling first.
  • Retest after each config change: nginx -t then reload.

Related setup guides you’ll likely need next

Rate limiting cuts abusive hits, but production setups still need clean TLS and sane DNS/email records.

These HostMyCode tutorials cover two common next steps:

Summary: keep the origin calm during bad traffic

A handful of Nginx directives can turn bot floods into fast 429 responses instead of slow PHP timeouts. Structure matters.

Define zones once, set reasonable defaults, then apply strict limits where attacks concentrate (login, admin, APIs).

If you want to run this on a server where you can scale CPU/RAM without changing your stack, start on a HostMyCode VPS.

If you host multiple client sites and prefer help with OS updates, Nginx tuning, and security guardrails, consider managed VPS hosting.

If your WordPress site or API is getting hammered by bots, rate limiting is one of the fastest fixes you can deploy without rewriting the app. HostMyCode offers VPS plans that give you full control of Nginx, with upgrade paths as traffic grows. See HostMyCode VPS or hand off the upkeep with managed VPS hosting.

FAQ

Will Nginx rate limiting break Googlebot or search indexing?

Usually not, as long as your global limits are moderate and you allow bursts. If you see 429s in search console logs, raise the burst or exempt known bot IP ranges (harder to maintain) rather than whitelisting by user-agent.

What’s a safe starting point for WordPress login limits?

A common baseline is 1r/s with a burst of 5–10 on /wp-login.php. If you still see CPU spikes during attacks, reduce burst or block /xmlrpc.php entirely if you don’t use it.

Should I rate limit by IP if many users share one NAT (office/mobile carrier)?

Be careful with strict limits on general browsing. Keep global limits gentle, and apply tight limits only to login/admin endpoints where NAT sharing is less likely to cause false positives.

Is rate limiting enough for DDoS protection?

It helps with layer‑7 abuse that reaches your Nginx. Large volumetric attacks still require upstream filtering (CDN, DDoS protection, or provider-level mitigation). Rate limiting remains useful even then, because it protects PHP and upstream apps.

How do I know if my limits are too strict?

Watch for spikes of 429 in /var/log/nginx/limited.log that correlate with user complaints. If real users report issues, increase burst first, then adjust the sustained rate.