Nginx Rate Limiting: Implementing IP-based Whitelisting and Custom Rate Rules


2 views

The limit_req directive in Nginx provides powerful request rate limiting capabilities, but many developers struggle when they need to implement more granular controls based on IP addresses. The standard implementation applies rate limits globally, which doesn't accommodate scenarios where you need to whitelist certain IPs or apply custom rate limits to specific addresses.

Many developers first attempt to solve this with conditional statements like:

if ($remote_addr = "1.2.3.4") {
    # This won't work for rate limiting!
    limit_req zone=mylimit burst=5 nodelay;
}

Nginx explicitly states in its documentation that limit_req cannot be used inside if blocks in location contexts. This limitation requires a different approach.

The proper way to implement IP-based rate limiting variations is through Nginx's geo module. Here's a complete implementation:

http {
    geo $rate_limit_key {
        default                 $binary_remote_addr;
        1.2.3.4/32              ""; # Whitelist this IP
        5.6.7.8/32              "slow"; # Special rate for this IP
    }
    
    map $rate_limit_key $limit {
        ""                      ""; # No limit
        "slow"                  $binary_remote_addr;
        default                 $binary_remote_addr;
    }
    
    limit_req_zone $limit zone=standard:10m rate=10r/s;
    limit_req_zone $limit zone=slow:10m rate=1r/s;
    
    server {
        location / {
            limit_req zone=standard burst=20 nodelay;
            
            if ($rate_limit_key = "slow") {
                limit_req zone=slow burst=5 nodelay;
            }
        }
    }
}

For simpler cases, you can use multiple location blocks with different rate limits:

server {
    location / {
        limit_req zone=standard burst=20 nodelay;
    }
    
    location @whitelist {
        # No rate limiting
    }
    
    location @slowlist {
        limit_req zone=slow burst=5 nodelay;
    }
}

Then use a map to route requests:

map $remote_addr $ratelimit_group {
    default          "standard";
    1.2.3.4          "whitelist";
    5.6.7.8          "slowlist";
}

When implementing IP-based rate limiting:

  • The geo module approach is more efficient for large IP lists
  • Consider using $binary_remote_addr instead of $remote_addr to save memory
  • Monitor your shared memory zone usage (zone=standard:10m)

After implementing, test with different IPs using curl:

# Test whitelisted IP
curl -H "X-Forwarded-For: 1.2.3.4" http://yourserver.com

# Test slow-limited IP  
curl -H "X-Forwarded-For: 5.6.7.8" http://yourserver.com

# Test default IP
curl http://yourserver.com

When implementing rate limiting in NGINX, we often need differential treatment for specific IP addresses. The standard limit_req directive applies globally, but production environments frequently require:

  • Complete whitelisting for trusted IPs
  • Custom rate limits for specific IP ranges
  • Different thresholds for API clients vs regular users

The most efficient approach uses NGINX's geo module to classify IPs before applying rate limits:

geo $rate_limit_key {
    default          $binary_remote_addr;
    192.168.1.0/24   "";
    203.0.113.42     "slow";
    198.51.100.0/24  "strict";
}

map $rate_limit_key $limit {
    ""       "";
    "slow"   "zone=slow:10m rate=1r/s";
    "strict" "zone=strict:10m rate=5r/m";
    default  "zone=global:10m rate=10r/s";
}

server {
    location / {
        limit_req $limit;
        # Your regular config
    }
}

For simpler cases with few IPs, you can use separate location blocks:

server {
    location / {
        limit_req zone=global burst=20 nodelay;
        
        # Whitelisted IPs
        if ($remote_addr ~ "^(192\.168\.1\.100|203\.0\.113\.42)$") {
            set $limit_req "";
        }
        
        # Strictly limited IPs
        if ($remote_addr ~ "^198\.51\.100\.(25[0-5]|2[0-4][0-9])$") {
            limit_req zone=strict burst=1;
        }
    }
}

For API clients, consider adding headers to communicate rate limits:

limit_req_status 429;
limit_req zone=global burst=20 nodelay;

add_header X-RateLimit-Limit "10r/s" always;
add_header X-RateLimit-Remaining $limit_req remaining always;

Verify your configuration with:

  1. nginx -t for syntax validation
  2. Tail error logs: tail -f /var/log/nginx/error.log
  3. Test with curl: curl -I http://yourserver.com