Imagine you set up two servers — Server A forwards requests to Server B, and Server B forwards them back to Server A. A single visitor request turns into an endless ping-pong match between your servers, spawning thousands of requests per second until something crashes. This is a request loop, and NGINX CDN loop detection is how you prevent it.
Request loops happen more often than you might expect. A DNS change, a CDN misconfiguration, or a simple typo in a proxy_pass directive can create one. The worst part? You often do not notice until your servers are already overwhelmed.
The NGINX loop detect module implements RFC 8586, which defines the CDN-Loop header — a simple counter that tracks how many times a request has bounced through your servers. When the counter gets too high, NGINX blocks the request and stops the loop.
Do You Need This if You Use Cloudflare?
Cloudflare already implements RFC 8586 and adds a CDN-Loop: cloudflare header to every request it forwards. If a request loops back through Cloudflare, they catch it and return a 403 error. So if Cloudflare is your only CDN layer, you already have basic loop protection.
However, the NGINX loop detect module is still valuable in these situations:
- NGINX sits between Cloudflare and your origin, and a misconfiguration sends requests from origin back to NGINX (bypassing Cloudflare). Cloudflare cannot catch loops it never sees.
- Multi-CDN setups where NGINX acts as one edge layer alongside Cloudflare or other CDN providers. Each layer needs its own loop tracking.
- No CDN at all — NGINX is your only reverse proxy and you want protection against internal routing loops between backend servers.
- Defense in depth — even with Cloudflare, catching loops at the NGINX layer means faster detection and less wasted bandwidth reaching Cloudflare and back.
Why NGINX CDN Loop Detection Matters
Request loops in CDN and reverse proxy setups typically occur in these scenarios:
| Scenario | How It Happens | Impact |
|---|---|---|
| Multi-CDN routing | CDN A forwards to CDN B, which routes back to CDN A | Exponential request multiplication |
| Origin misconfiguration | Origin server redirects back through the CDN | CPU and bandwidth exhaustion |
| DNS failover loops | Failover target points back to the primary | Cascading infrastructure failure |
| Load balancer chains | Nested load balancers create circular paths | Connection pool exhaustion |
Without NGINX CDN loop detection, a single looping request can multiply into thousands of internal requests within seconds. Implementing loop prevention protects your servers from this resource drain.
Native NGINX Alternatives
NGINX has built-in protection against internal redirect loops via the internal directive and a hardcoded limit of 10 internal redirects. However, NGINX provides no native mechanism for detecting external request loops. These are requests that leave the server and return through the network. The loop detect module fills this gap with the standardized CDN-Loop header protocol. If you are looking for general NGINX security hardening, loop detection is one more layer to add to your defense.
How the Module Works
The NGINX CDN loop detection module operates in three stages:
- Parsing: When a request arrives, the module inspects the
CDN-Loopheader for an entry matching your configured CDN identifier. The header follows the format:CDN-Loop: my_cdn; loops=3, other_cdn; loops=1 -
Tracking: The module extracts the current hop count for your CDN identifier. It then constructs an updated
CDN-Loopheader value with the count incremented by one. -
Blocking: If the hop count exceeds the configured maximum, NGINX returns an error status code (508 by default). It also logs a detailed error message for diagnostics.
The module registers its handler in the NGINX access phase. It evaluates every request before the content handler runs. This ensures that looping requests are blocked before they reach your upstream servers.
Important: The
returndirective in NGINX short-circuits processing before the access phase. If you usereturnin a location block, automatic loop blocking will not trigger. Useproxy_passor similar content handlers for effective NGINX CDN loop detection. The module’s variables ($loop_detect_current_loops) remain available everywhere.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install the module from the GetPageSpeed RPM repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-loop-detect
After installation, load the module by adding this line at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_loop_detect_module.so;
Alternatively, include all installed module configurations:
include /usr/share/nginx/modules/*.conf;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-loop-detect
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Configuration Directives
loop_detect
Syntax: loop_detect on | off;
Default: loop_detect off;
Context: http, server, location
Enables or disables CDN loop detection. When enabled, the module inspects the CDN-Loop header on every request. It blocks those exceeding the configured hop limit. You can enable it globally in the http block or selectively per server or location.
http {
# Enable globally
loop_detect on;
loop_detect_cdn_id my_cdn;
server {
listen 80;
# Inherited from http block
location / {
proxy_pass http://backend;
}
}
}
loop_detect_cdn_id
Syntax: loop_detect_cdn_id <identifier>;
Default: loop_detect_cdn_id openresty;
Context: http, server, location
Sets the unique identifier for your CDN node or cluster. The module uses this identifier to locate its entry in the CDN-Loop header. Choose a descriptive, unique name that distinguishes your infrastructure from other CDN providers.
loop_detect_cdn_id my_company_edge;
Best practice: Use a consistent identifier across all NGINX instances in your CDN cluster. This ensures the module tracks loops correctly, regardless of which node processes the request.
loop_detect_status
Syntax: loop_detect_status <code>;
Default: loop_detect_status 508;
Context: http, server, location
Sets the HTTP status code returned when a request exceeds the loop limit. The code must be between 400 and 599. The default 508 (Loop Detected) is semantically appropriate, as it communicates that a circular dependency was found.
# Use 429 Too Many Requests instead
loop_detect_status 429;
NGINX validates this value at configuration time. A status code outside 400–599 causes nginx -t to fail:
value must be between 400 and 599
loop_detect_max_allow_loops
Syntax: loop_detect_max_allow_loops <number>;
Default: loop_detect_max_allow_loops 10;
Context: http, server, location
Sets the maximum number of allowed hops before blocking. The module blocks requests where the loop count is strictly greater than this value. With loop_detect_max_allow_loops 3, a request with loops=3 passes, but loops=4 is blocked.
# Allow up to 5 hops through your CDN
loop_detect_max_allow_loops 5;
Choose this value based on your architecture. A single-proxy setup needs only 1–2 allowed loops. Multi-tier CDN architectures may need higher values. Keeping this value low provides faster loop detection.
Variables
The module exports two variables for logging, header manipulation, and conditional logic.
$loop_detect_current_loops
Contains the current hop count from the incoming CDN-Loop header. Returns 0 if the header is absent, empty, or contains no matching CDN identifier. Use it for monitoring:
# Log the loop count for every request
log_format cdn '$remote_addr - $request - loops=$loop_detect_current_loops';
access_log /var/log/nginx/cdn.log cdn;
$loop_detect_proxy_add_cdn_loop
Constructs the updated CDN-Loop header value for the upstream server. It includes your CDN identifier with the loop count incremented by one, plus any other CDN entries from the original header.
For example, if the incoming header is CDN-Loop: my_cdn; loops=2, other_cdn; loops=1, this variable produces my_cdn; loops=3, other_cdn; loops=1.
You must set this as a proxy header for the detection chain to work:
location / {
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
proxy_pass http://upstream;
}
Without forwarding this header, downstream servers cannot track the loop count.
Practical Configuration Examples
Basic Reverse Proxy with Loop Protection
This is the simplest configuration for a single reverse proxy server:
load_module modules/ngx_http_loop_detect_module.so;
events {}
http {
loop_detect on;
loop_detect_cdn_id my_proxy;
loop_detect_max_allow_loops 5;
upstream backend {
server 10.0.0.10:80;
server 10.0.0.11:80;
}
server {
listen 80;
server_name example.com;
location / {
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
proxy_set_header Host $host;
proxy_pass http://backend;
}
}
}
Multi-Tier CDN with Monitoring
For complex CDN deployments, combine NGINX CDN loop detection with detailed logging to monitor hop counts:
load_module modules/ngx_http_loop_detect_module.so;
events {}
http {
log_format cdn_monitor '$remote_addr [$time_local] "$request" $status '
'cdn_loops=$loop_detect_current_loops '
'upstream=$upstream_addr';
loop_detect on;
loop_detect_cdn_id edge_cluster_us;
loop_detect_max_allow_loops 3;
loop_detect_status 508;
upstream origin {
server origin.internal:8080;
}
server {
listen 80;
server_name cdn.example.com;
access_log /var/log/nginx/cdn_access.log cdn_monitor;
location / {
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://origin;
}
}
}
With this configuration, you can monitor loop counts in your access logs:
192.168.1.100 [14/Mar/2026:12:00:01 +0000] "GET / HTTP/1.1" 200 cdn_loops=0 upstream=10.0.0.10:8080
192.168.1.101 [14/Mar/2026:12:00:02 +0000] "GET /api HTTP/1.1" 508 cdn_loops=4 upstream=-
Per-Location Loop Limits
You can set different loop limits for different parts of your application. API endpoints that should never loop benefit from stricter limits:
server {
listen 80;
server_name example.com;
loop_detect on;
loop_detect_cdn_id my_cdn;
location / {
loop_detect_max_allow_loops 5;
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
proxy_pass http://content_backend;
}
location /api/ {
loop_detect_max_allow_loops 1;
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
proxy_pass http://api_backend;
}
}
Performance Considerations
The loop detect module adds minimal overhead to request processing:
- Header parsing happens once per request during the access phase. The parser scans the
CDN-Loopheader linearly, making it O(n) in the number of CDN entries. - Memory allocation is limited to a small per-request context structure. Both the context and header value are allocated from the request pool and freed automatically.
- No external dependencies: The module makes no network calls and reads no files. It operates entirely on request headers.
For most deployments, the performance impact is negligible. Moreover, preventing runaway loops saves far more resources than the module consumes.
Troubleshooting
Loop Detection Not Triggering
Symptom: Requests with high loop counts pass through without being blocked.
Diagnosis: Check whether the location uses return or rewrite directives:
nginx -T | grep -A 5 "location.*/"
Fix: The blocking handler runs in the access phase. If return processes the request first, blocking never occurs. Use the variable with a map block instead:
map $loop_detect_current_loops $is_loop {
default 0;
"~^[4-9]$" 1;
"~^[0-9]{2,}$" 1;
}
server {
listen 80;
loop_detect on;
loop_detect_cdn_id my_cdn;
location / {
if ($is_loop) {
return 508;
}
return 200 "OK";
}
}
CDN-Loop Header Not Forwarded
Symptom: Downstream servers show loops=0 even after multiple hops.
Diagnosis: Verify the proxy_set_header CDN-Loop directive exists:
nginx -T | grep -i cdn-loop
Fix: Add the header to every location that uses proxy_pass:
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
Wrong CDN Identifier
Symptom: The module reports loops=0 even though CDN-Loop headers exist in the request.
Diagnosis: The identifier comparison is case-insensitive but must otherwise match exactly. Test it:
curl -s -H "CDN-Loop: My_CDN; loops=5" http://localhost/info
Fix: Ensure all NGINX instances use the same loop_detect_cdn_id value.
Checking the Error Log
When the module blocks a request, it logs at the error level:
loop_detect: request loops exceeded the limit, current_loops: 4, max_allow_loops: 3
Monitor these entries to detect loop conditions:
grep "loop_detect" /var/log/nginx/error.log
Security Best Practices
Choose a Non-Guessable CDN Identifier
A predictable identifier makes it easier for attackers to craft requests that bypass detection. Use a unique, non-obvious identifier:
loop_detect_cdn_id edge_7f3a9c;
Set Conservative Loop Limits
Most legitimate request chains traverse fewer than 3 CDN nodes. Set loop_detect_max_allow_loops as low as your architecture allows:
# Strict limit for single-proxy setups
loop_detect_max_allow_loops 2;
Monitor Loop Counts
Review your access logs for requests with non-zero loop counts. A sudden increase often indicates a misconfiguration or an attack:
awk '/cdn_loops=[1-9]/' /var/log/nginx/cdn_access.log | tail -20
Protect the CDN-Loop Header
If your NGINX instance is the first entry point, the module constructs the header from scratch. This naturally prevents external manipulation of the loop count for your identifier:
# At the edge — header is built fresh
proxy_set_header CDN-Loop $loop_detect_proxy_add_cdn_loop;
Conclusion
The NGINX loop detect module provides a lightweight, standards-based solution for NGINX CDN loop detection in proxy deployments. By implementing RFC 8586, it ensures interoperability with other CDN providers while protecting your infrastructure. The module requires minimal configuration and adds negligible performance overhead. It integrates seamlessly with NGINX’s existing access control and header management.
For the source code and to report issues, visit the GitHub repository. The module is available as a pre-built package from the GetPageSpeed repository for RPM-based distributions.

