Skip to main content

Rate Limiting: Request Rate Control

Rate Limiting restricts the number of requests processed per unit time, making it a core security feature for defending against DDoS attacks, brute force attacks, and web scraping.


Nginx Rate Limiting​

Nginx provides two types of limits: limit_req (request rate) and limit_conn (concurrent connections).

limit_req_zone β€” Request Rate Limiting​

# /etc/nginx/nginx.conf β€” http block

http {
# Define request rate limit zones
# key: $binary_remote_addr = client IP (binary form, memory-efficient)
# zone=api_limit:10m = zone name:memory size (10MB β‰ˆ 160,000 IPs)
# rate=10r/s = allow 10 requests per second

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m; # 5 per minute
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;

# Status code returned when limit is exceeded (default: 503)
limit_req_status 429; # Too Many Requests
limit_conn_status 429;
}
server {
# General pages: 30 requests per second per IP
location / {
limit_req zone=general burst=50 nodelay;
proxy_pass http://backend;
}

# API: 10 requests per second per IP, burst of 20
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}

# Login: 5 requests per minute per IP
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://backend;
}
}

Understanding burst and nodelay:

With rate=10r/s  burst=20  nodelay:
─────────────────────────────────────────
Process up to 20 requests instantly (nodelay)
Then process at 10 requests per second
Return 429 when burst is exceeded

Without nodelay: queue requests within burst at the rate speed (delayed processing)

limit_conn β€” Concurrent Connection Limit​

http {
# Concurrent connection limit zone
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
}

server {
location / {
# Maximum 10 concurrent connections per IP
limit_conn conn_limit 10;
proxy_pass http://backend;
}

# File downloads: connection + bandwidth limit
location /downloads/ {
limit_conn conn_limit 3;
limit_rate 1m; # 1MB/s per connection bandwidth limit
limit_rate_after 10m; # First 10MB unlimited, then limited
alias /var/www/downloads/;
}
}

Exclude Whitelisted IPs from Rate Limiting​

http {
# Exclude internal networks from rate limiting
geo $limit {
default $binary_remote_addr;
127.0.0.1 ""; # Empty string β†’ rate limit not applied
10.0.0.0/8 "";
192.168.0.0/16 "";
}

limit_req_zone $limit zone=api_limit:10m rate=10r/s;
}

Apache Rate Limiting​

mod_ratelimit​

sudo a2enmod ratelimit
<Location "/api/">
# Transfer rate limit (bytes/second)
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 400 # 400KB/s
</Location>

mod_evasive β€” HTTP Flood Defense​

sudo apt install libapache2-mod-evasive
sudo a2enmod evasive
# /etc/apache2/mods-available/evasive.conf

<IfModule mod_evasive20.c>
# Same-page request threshold: number of requests to the same URI within 2 seconds
DOSPageCount 5
DOSSiteCount 50

# Blacklist retention time when threshold is exceeded (seconds)
DOSPageInterval 2
DOSSiteInterval 1
DOSBlockingPeriod 10

# Email notification on block
DOSEmailNotify admin@example.com

# Whitelist IPs
DOSWhitelist 127.0.0.1
DOSWhitelist 10.0.0.*

# Log directory
DOSLogDir /var/log/mod_evasive
</IfModule>

Rate Limiting in Spring Boot​

Nginx rate limiting applies to all requests, but you can also implement additional controls at the application level.

// Using the Bucket4j library (add dependency)
// implementation 'com.bucket4j:bucket4j-core:8.x.x'

@RestController
@RequestMapping("/api")
public class ApiController {

private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

private Bucket getBucket(String ip) {
return buckets.computeIfAbsent(ip, k ->
Bucket.builder()
.addLimit(Bandwidth.classic(10, Refill.greedy(10, Duration.ofSeconds(1))))
.build()
);
}

@GetMapping("/products")
public ResponseEntity<?> getProducts(HttpServletRequest request) {
String ip = request.getRemoteAddr();
Bucket bucket = getBucket(ip);

if (bucket.tryConsume(1)) {
return ResponseEntity.ok(productService.findAll());
}

return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("X-Rate-Limit-Retry-After-Seconds", "1")
.body("Too many requests");
}
}

Customizing Rate Limit Responses​

# Add Retry-After header to 429 responses
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;

# Customize 429 response
error_page 429 @rate_limited;
}

location @rate_limited {
add_header Retry-After 1 always;
add_header Content-Type "application/json" always;
return 429 '{"error":"Too Many Requests","retryAfter":1}';
}
}

Rate Limiting Monitoring​

# Check frequency of 429 errors
grep " 429 " /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# Enable rate limit logging
http {
log_format rate_log '$remote_addr [$time_local] "$request" $status '
'limit_req=$limit_req_status';
# $limit_req_status: PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN, REJECTED_DRY_RUN

access_log /var/log/nginx/rate.log rate_log;
}

# Real-time monitoring of blocked IPs
tail -f /var/log/nginx/access.log | grep " 429 "

EndpointzonerateburstNotes
General pagesgeneral30r/s50Set higher behind CDN
APIapi_limit10r/s20Adjust to service characteristics
Loginlogin_limit5r/m3Brute force defense
Searchsearch_limit5r/s10Consider DB load
File downloadsdownload2r/s5Limit with bandwidth