Nginx Rate Limiting Blocking Legitimate API Users Behind Cloudflare IP
When Nginx rate limiting seems to block far more legitimate traffic than expected, especially many different users seemingly all hitting the limit simultaneously, the cause is very often that Nginx is rate limiting based on Cloudflare's own proxy IP address rather than each individual visitor's real IP — effectively treating every visitor coming through Cloudflare as if they were a single client.
The Problem
With Cloudflare (or a similar CDN/proxy) in front of your Nginx server, rate limiting configured to allow a reasonable number of requests per client instead blocks legitimate users broadly:
429 Too Many Requests
Multiple genuinely different users report being rate-limited around the same time, even though none of them individually made anywhere near the configured request threshold.
Why It Happens
When traffic passes through Cloudflare (or any reverse proxy/CDN), Nginx by default sees the connecting IP as Cloudflare's own proxy IP, not the original visitor's real IP address — Cloudflare passes the real client IP separately, in the CF-Connecting-IP header (or the more general X-Forwarded-For), but Nginx doesn't automatically use this for its own rate limiting logic unless explicitly configured to. Without that configuration, Nginx's limit_req_zone, keyed on $remote_addr, sees every visitor coming through Cloudflare as originating from the same small set of Cloudflare edge IPs, effectively aggregating rate limits across many unrelated real users into what looks like one very high-traffic "client."
The Fix
Configure Nginx to use the real visitor IP (from Cloudflare's header) for rate limiting, rather than the connecting IP, which is Cloudflare's own proxy address. This requires the ngx_http_realip_module (included in most standard Nginx builds, but confirm it's compiled in if using a custom build):
real_ip_header CF-Connecting-IP;
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;
# ... additional Cloudflare IP ranges
The set_real_ip_from directives must list Cloudflare's actual published IP ranges — Nginx only trusts the CF-Connecting-IP header when the request genuinely originates from one of these trusted ranges, which prevents a malicious client from simply spoofing the header themselves to bypass rate limiting entirely. Cloudflare publishes their current IP ranges directly, and this list should be kept up to date, since Cloudflare does occasionally add new ranges.
With real_ip_module correctly configured, $remote_addr now correctly reflects each individual visitor's real IP rather than Cloudflare's proxy IP, and your existing rate limiting configuration automatically benefits without needing further changes to the limit_req_zone itself:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
}
Verify the fix by checking what IP Nginx actually sees for incoming requests after the configuration change, confirming it now shows real, varied visitor IPs rather than a small, repeated set of Cloudflare edge IPs:
log_format real_ip_test '$remote_addr - $http_cf_connecting_ip - $request';
access_log /var/log/nginx/realip_test.log real_ip_test;
Reviewing this temporary log confirms whether $remote_addr now matches the value in CF-Connecting-IP, which is the expected, correctly configured state — if they still differ, the real_ip configuration isn't being applied correctly, possibly due to the requesting IP not actually matching one of your configured set_real_ip_from ranges.
Still Not Working?
If you're running Nginx behind multiple layers of proxying (for example, Cloudflare in front of a separate internal load balancer, which then forwards to Nginx), confirm which specific header actually contains the true original client IP by that point in the chain, since intermediate proxies sometimes need their own explicit configuration to correctly forward or append to X-Forwarded-For rather than overwriting it with their own IP. In multi-hop scenarios, checking the actual header values received at each stage (temporarily logging them, as shown above, at each layer if you control the configuration) is the most reliable way to confirm exactly where in the chain the real client IP information is being lost or incorrectly handled, rather than assuming based on how a single-proxy setup would typically behave.