Security

Nginx Rate Limiting: Prevent Brute Force and API Abuse

May 16, 2026

Back to Blog

Why Rate Limiting Is Essential for Every Server

Every server connected to the internet faces a constant barrage of automated attacks. Brute force login attempts, credential stuffing, API scraping, and distributed denial-of-service (DDoS) attacks are not a question of "if" but "when." Without proper rate limiting, a single malicious actor can overwhelm your server resources, lock out legitimate users, and potentially gain unauthorized access to your systems.

Nginx rate limiting is one of the most effective first lines of defense. By controlling how many requests a client can make within a given time window, you can neutralize brute force attacks, protect API endpoints from abuse, and ensure fair resource distribution among all visitors. The best part? Nginx handles rate limiting at the connection level before requests even reach your application, making it incredibly efficient.

The Cost of No Rate Limiting: A single attacker running Hydra or similar tools can attempt thousands of password combinations per minute against your login page. Without rate limiting, your server will dutifully process every single one, consuming CPU, memory, and bandwidth while potentially finding a valid password.

Understanding Nginx Rate Limiting Architecture

Nginx rate limiting operates on two fundamental concepts: shared memory zones and rate enforcement directives. The shared memory zone stores the state of each client (typically identified by IP address), while the enforcement directives define the rules that apply to specific locations in your configuration.

Client Request
Shared Memory Zone
IP tracking
Rate Check
Allow / Reject (429)

The algorithm Nginx uses is called the leaky bucket. Think of it as a bucket with a small hole at the bottom. Water (requests) flows in from the top, and drains out at a fixed rate through the hole. If water comes in faster than it drains, the bucket fills up. Once full, any additional water overflows and is rejected. The bucket size is the "burst" parameter, and the drain rate is the defined rate limit.

Core Directives: limit_req_zone and limit_req

The foundation of Nginx rate limiting is the limit_req_zone directive, which must be placed in the http block of your configuration. This directive defines three things: the key to identify clients, the shared memory zone size, and the rate.

# In the http {} block of nginx.conf limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s; limit_req_zone $binary_remote_addr zone=api:20m rate=30r/s; limit_req_zone $binary_remote_addr zone=general:10m rate=50r/s;

Let us break down each component:

ComponentExamplePurpose
$binary_remote_addrClient IP (4 bytes)Identifies each client; uses 4 bytes per IPv4 address (vs 15 bytes for $remote_addr string)
zone=login:10m10MB shared memoryStores ~160,000 IP states at ~64 bytes each
rate=5r/s5 requests/secondMaximum sustained rate; can also use r/m for per-minute rates

Once you have defined the zone, apply it to specific locations using the limit_req directive:

# Protect login page location /wp-login.php { limit_req zone=login burst=10 nodelay; limit_req_status 429; # ... proxy_pass or fastcgi_pass }

Burst and Nodelay: Fine-Tuning the Leaky Bucket

The burst parameter and nodelay option are critical for balancing security with user experience. Without understanding them, you will either block legitimate users or leave gaps in your protection.

Without Burst

Strictly enforces the rate. If the rate is 5r/s, the 6th request within one second is immediately rejected with a 503 (or 429 if configured). This is too aggressive for most real-world scenarios because browsers often make multiple simultaneous requests.

Too strict for production

With Burst (no nodelay)

burst=10 allows 10 excess requests to queue up. They are processed at the defined rate, meaning they experience a delay. A burst of 10 at 5r/s means queued requests wait up to 2 seconds. Good for forms, problematic for APIs.

Adds latency

The Best of Both Worlds — burst + nodelay: limit_req zone=api burst=20 nodelay; This allows 20 requests to be processed immediately (no queuing delay), but then enforces the rate for subsequent requests. The burst "slots" refill at the defined rate. This is the recommended setting for most use cases.

Connection Limiting with limit_conn

While limit_req controls the request rate, limit_conn controls the number of simultaneous connections from a single client. This is particularly effective against slowloris attacks and download abuse.

# In http {} block limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m; limit_conn_zone $server_name zone=conn_per_server:10m; # In server {} or location {} block limit_conn conn_per_ip 30; limit_conn conn_per_server 500; limit_conn_status 429;
DirectiveControlsBest For
limit_reqRequests per secondBrute force prevention, API rate limiting
limit_connConcurrent connectionsSlowloris defense, download throttling

Rate Limiting Per URI, IP, and Custom Headers

The key variable in limit_req_zone is not limited to the client IP. You can create sophisticated rate limiting rules based on different identifiers:

Rate Limiting by URI

# Rate limit per IP per URI limit_req_zone $binary_remote_addr$uri zone=per_uri:20m rate=10r/s;

Rate Limiting by API Key

# Map API key from header map $http_x_api_key $api_client { default $binary_remote_addr; "~^[a-zA-Z0-9]+" $http_x_api_key; } limit_req_zone $api_client zone=api_keyed:20m rate=100r/s;

Rate Limiting Behind a Reverse Proxy

Critical Warning: If your server sits behind a load balancer or CDN (Cloudflare, AWS ALB), $binary_remote_addr will be the proxy IP, not the real client. You must use the X-Forwarded-For header or Cloudflare's CF-Connecting-IP instead. Failing to do this means ALL visitors share the same rate limit!
# Use real IP behind Cloudflare 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; # ... add all Cloudflare IP ranges real_ip_header CF-Connecting-IP; # Now $binary_remote_addr is the real client IP limit_req_zone $binary_remote_addr zone=general:10m rate=50r/s;

Custom 429 Error Pages

By default, Nginx returns a 503 Service Unavailable when a rate limit is hit. This is misleading. The proper HTTP status code is 429 Too Many Requests. Configure it properly and serve a user-friendly error page:

limit_req_status 429; limit_conn_status 429; error_page 429 /429.html; location = /429.html { root /var/www/error-pages; internal; add_header Retry-After 60 always; }

The Retry-After header tells well-behaved clients how long to wait before retrying. APIs should always include this header in 429 responses.

Whitelisting Trusted IPs

You do not want to rate-limit your own monitoring systems, internal services, or trusted partners. Use the geo module to create a whitelist:

# Define trusted IPs geo $rate_limit_key { default $binary_remote_addr; 10.0.0.0/8 ""; 192.168.0.0/16 ""; 203.0.113.50 ""; # Monitoring server } # Empty key = no rate limiting limit_req_zone $rate_limit_key zone=general:10m rate=50r/s;

When the key is an empty string, Nginx skips rate limiting entirely for that request. This is elegant because it requires no additional if directives or complex logic.

Real-World Configuration: Protecting Different Endpoints

Different parts of your application need different rate limits. Here is a complete, production-ready configuration:

Login and Authentication Pages

# Strict rate limiting for auth endpoints limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s; location ~ ^/(wp-login\.php|login|admin/login|user/login) { limit_req zone=auth burst=5 nodelay; limit_req_status 429; limit_req_log_level warn; # ... pass to backend }

API Endpoints

# Tiered API rate limiting limit_req_zone $binary_remote_addr zone=api_read:20m rate=60r/s; limit_req_zone $binary_remote_addr zone=api_write:10m rate=10r/s; location /api/v1/ { limit_req zone=api_read burst=30 nodelay; limit_req_status 429; } location ~ ^/api/v1/.+/(create|update|delete) { limit_req zone=api_write burst=5 nodelay; limit_req_status 429; }

Static Assets (Lenient)

# Generous limits for static files location ~* \.(css|js|jpg|png|gif|ico|woff2|svg)$ { limit_req zone=general burst=100 nodelay; expires 30d; add_header Cache-Control "public, immutable"; }

Testing Rate Limits

Never deploy rate limits without testing. Use tools like ab (Apache Bench), wrk, or curl to verify your configuration works as expected.

Testing with Apache Bench

# Send 100 requests, 10 concurrent $ ab -n 100 -c 10 https://example.com/wp-login.php Complete requests: 100 Non-2xx responses: 87 Percentage of requests served within a certain time (ms) 50% 12 90% 15 99% 22 # Check Nginx error log for rate limiting entries $ tail -f /var/log/nginx/error.log | grep "limiting" limiting requests, excess: 5.432 by zone "auth", client: 203.0.113.10

Testing with wrk

# Sustained load test: 4 threads, 50 connections, 30 seconds $ wrk -t4 -c50 -d30s https://example.com/api/v1/status Running 30s test @ https://example.com/api/v1/status 4 threads and 50 connections Requests/sec: 60.23 Non-2xx or 3xx responses: 1247
Tip: Watch for Non-2xx responses in the output. These are the requests that hit your rate limit. The ratio between successful and rejected requests tells you if your limits are appropriately tuned.

Combining Rate Limiting with Fail2ban

Rate limiting alone stops individual requests, but a persistent attacker can keep hitting your 429 limit indefinitely. Combine Nginx rate limiting with Fail2ban to automatically ban repeat offenders at the firewall level.

1
Enable rate limit logging in Nginx:
Add limit_req_log_level warn; to your server block. Nginx will log every rate-limited request to the error log.
2
Create a Fail2ban filter:
Create /etc/fail2ban/filter.d/nginx-ratelimit.conf with a regex that matches Nginx rate limiting log entries.
# /etc/fail2ban/filter.d/nginx-ratelimit.conf [Definition] failregex = limiting requests, excess:.* by zone .*, client: <HOST> ignoreregex =
3
Create a Fail2ban jail:
Add a jail that monitors the Nginx error log and bans IPs that trigger rate limits too frequently.
# /etc/fail2ban/jail.d/nginx-ratelimit.conf [nginx-ratelimit] enabled = true filter = nginx-ratelimit action = nftables-allports[name=nginx-ratelimit] logpath = /var/log/nginx/error.log maxretry = 20 findtime = 60 bantime = 3600

This configuration bans any IP that triggers the rate limit 20 times within 60 seconds, blocking them at the firewall level for one hour. The attacker cannot even reach Nginx anymore.

Advanced: Multiple Zone Stacking

Nginx allows you to apply multiple rate limits to the same location. This is powerful for implementing tiered protection:

# Two zones: per-IP and global limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s; limit_req_zone $server_name zone=per_server:10m rate=1000r/s; location /api/ { limit_req zone=per_ip burst=20 nodelay; limit_req zone=per_server burst=200 nodelay; }

With this configuration, each individual IP is limited to 10 requests per second, but the entire server location is also capped at 1,000 requests per second total. Even if 200 different IPs each send 10 requests per second, the global limit prevents the server from being overwhelmed.

Rate Limiting Configurations Summary

Endpoint TypeRecommended RateBurstNodelay
Login/Auth pages3-5 r/s5-10Yes
Password reset1 r/s3Yes
API read endpoints30-60 r/s20-30Yes
API write endpoints5-10 r/s5Yes
Search/autocomplete10-20 r/s15Yes
File uploads2-5 r/s3No
Static assets100-200 r/s100Yes
Webhooks (incoming)20-50 r/s10Yes

Monitoring and Logging

Effective rate limiting requires ongoing monitoring. Configure logging levels and analyze patterns to fine-tune your limits over time.

# Log rate-limited requests with custom format log_format ratelimit '$remote_addr - [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" $limit_req_status'; access_log /var/log/nginx/ratelimit.log ratelimit if=$limit_req_status; # Analyze most rate-limited IPs $ awk '{print $1}' /var/log/nginx/ratelimit.log | sort | uniq -c | sort -rn | head -20 4521 185.220.101.42 2103 45.134.26.19 891 103.152.220.88
Pro Tip: Review your rate limiting logs weekly. If legitimate users are being rate-limited frequently, increase the burst parameter. If you see IPs consistently hitting limits, add them to Fail2ban or block them at the firewall level permanently.

Common Pitfalls and Troubleshooting

Pitfall: Rate Limiting Static Files Too Aggressively

A single page load can trigger 30-50 requests for CSS, JS, images, and fonts. If your general rate limit is too low, legitimate page loads will fail. Either exclude static files from rate limiting or use a very generous limit.

Pitfall: Forgetting About AJAX/XHR

Modern web applications make frequent AJAX requests (notifications, live updates, search autocomplete). These count against rate limits. Monitor your application's actual request patterns before setting limits.

Pitfall: Shared IP Environments

Users behind corporate NATs, university networks, or mobile carriers may share a single IP. Rate limiting by IP alone can block entire organizations. Consider implementing token-based rate limiting for authenticated endpoints.

Pitfall: Not Testing Under Load

Rate limits that seem reasonable in theory can break under real traffic patterns. Always load-test your configuration before deploying to production. Use staging environments that mirror production traffic.

Complete Production Configuration

Here is a comprehensive Nginx rate limiting configuration you can adapt for your server:

# === Rate Limiting Zones (http block) === # Whitelist trusted IPs geo $rate_limit { default $binary_remote_addr; 10.0.0.0/8 ""; 127.0.0.1 ""; } limit_req_zone $rate_limit zone=auth:10m rate=3r/s; limit_req_zone $rate_limit zone=api:20m rate=30r/s; limit_req_zone $rate_limit zone=general:10m rate=50r/s; limit_conn_zone $rate_limit zone=conn:10m; limit_req_status 429; limit_conn_status 429; # === Server block === server { limit_conn conn 30; limit_req zone=general burst=50 nodelay; location ~ ^/(wp-login|login|admin) { limit_req zone=auth burst=5 nodelay; } location /api/ { limit_req zone=api burst=20 nodelay; } error_page 429 /429.html; location = /429.html { root /var/www/error-pages; internal; add_header Retry-After 60 always; } }

How Panelica Handles Rate Limiting

Panelica's Nginx configuration includes built-in rate limiting per domain, configurable through the panel. When you create or manage a domain in Panelica, rate limiting zones are automatically provisioned with sensible defaults. You can adjust rates for different URL patterns directly from the domain settings without manually editing Nginx configuration files.

Combined with Fail2ban integration and the ModSecurity WAF, Panelica provides layered protection against abuse. The security stack works together: rate limiting catches burst attacks, Fail2ban escalates repeat offenders to firewall bans, and ModSecurity inspects request content for malicious payloads. This defense-in-depth approach means that even if one layer is bypassed, the others continue to protect your server.

  • Per-domain rate limiting configured through the panel UI
  • Automatic Fail2ban integration for repeat offenders
  • ModSecurity WAF with OWASP Core Rule Set for deep inspection
  • nftables firewall with IP blocking and country-based rules
  • Real-time security logs and audit trails in the dashboard

Key Takeaways

Rate limiting is not a set-and-forget solution. It requires understanding your application's traffic patterns, testing under realistic conditions, and ongoing monitoring. Start with conservative limits, monitor for false positives, and adjust gradually. Combine Nginx rate limiting with Fail2ban for automatic IP banning and use connection limits alongside request limits for comprehensive protection.

The difference between a server that survives a brute force attack and one that crumbles is often just a few lines of Nginx configuration. Invest the time to implement rate limiting properly, and your server will thank you by staying online when it matters most.

Share:
Security, built-in.