Rate Limiting Per IP Failing Behind Cloudflare/Reverse Proxy (Incorrect Client IP)
Quick answer
Application-level rate limiting that seems to treat every visitor as the same client, or that's trivially bypassable, almost always means your framework is...
Application-level rate limiting that seems to treat every visitor as the same client, or that's trivially bypassable, almost always means your framework is reading the proxy's own connecting IP rather than the real visitor's IP — the fix requires configuring your specific framework's trusted-proxy mechanism correctly, since the exact configuration differs meaningfully across different languages and frameworks.
The Problem
Rate limiting configured to allow a reasonable number of requests per individual client instead behaves as if every visitor is one client, or fails to actually differentiate between clearly distinct users:
# Application logs show every request appearing to come from the same IP,
# regardless of the actual visitor's real, distinct IP address
req.ip -> "104.16.132.229" (Cloudflare's edge IP, not the real visitor)
Why It Happens
When traffic passes through Cloudflare, another CDN, or any reverse proxy, the connection your application server actually receives comes from that intermediary's own IP address, not the original visitor's — the real visitor IP is passed separately in a header (commonly X-Forwarded-For or a provider-specific header like CF-Connecting-IP), but your application framework doesn't automatically know to trust and use this header unless explicitly configured to do so. Without this configuration, rate limiting keyed on "the connecting IP" effectively keys on the proxy's IP for every single request, aggregating every distinct visitor's rate limit into what looks like one very high-traffic client.
The Fix
The exact configuration mechanism differs by framework, but the underlying principle is universal: explicitly tell your application which proxy IPs to trust, and which specific header to read the real client IP from.
For Express (Node.js), configure the trust proxy setting, which then makes req.ip correctly resolve to the real client IP from X-Forwarded-For, as long as the request genuinely came through a trusted proxy hop:
app.set('trust proxy', 1); // trust exactly one hop (adjust based on your actual proxy chain)
For Django (Python), configure SECURE_PROXY_SSL_HEADER alongside middleware or a package like django-xff to correctly parse the real client IP from forwarded headers, since Django's own request.META['REMOTE_ADDR'] doesn't automatically account for proxying without explicit configuration:
# settings.py, using django-xff or equivalent
XFF_TRUSTED_PROXY_DEPTH = 1
For Ruby on Rails, the framework generally handles X-Forwarded-For parsing automatically via request.remote_ip, but confirm your specific rate-limiting gem (like rack-attack) is actually using this correctly-resolved value rather than a lower-level, unproxied IP source:
Rack::Attack.throttle('requests by ip', limit: 100, period: 60) do |req|
req.ip # confirm this resolves the real client IP correctly in your specific setup
end
Regardless of framework, always specifically restrict trust to your actual known proxy IP ranges rather than trusting the forwarded header unconditionally from any source — an application that blindly trusts X-Forwarded-For from any connecting IP, not just from your genuine reverse proxy, allows any external client to simply set their own fake header value and completely bypass IP-based rate limiting by spoofing an arbitrary claimed IP:
# Cloudflare specifically publishes their current IP ranges -
# only trust the forwarded header when the actual connecting IP
# is genuinely within Cloudflare's published range
After configuring your framework's trusted proxy settings correctly, verify the fix by logging the resolved client IP across several genuinely distinct real requests, confirming each shows a different, correct, real visitor IP rather than a single repeated proxy IP:
console.log('Resolved client IP:', req.ip);
Still Not Working?
If you've configured trusted proxy settings correctly for a single proxy hop but requests actually pass through multiple layers (Cloudflare, then an internal load balancer, then your application server), confirm the trusted proxy depth/count setting matches the actual number of hops in your specific infrastructure — a mismatch here (configuring for one hop when there are genuinely two) can result in either still reading an intermediate proxy's IP rather than the true original client, or, in the opposite misconfiguration, trusting a header value that a client could have set themselves before it ever reached your legitimate first proxy hop. Reviewing your actual full infrastructure's request path, not just assuming a single-hop configuration, ensures the trusted-hop count genuinely matches your real deployment topology.