Skip to main content

NGINX / Security

NGINX CDN Loop Detection: Prevent Request Loops

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

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:

  1. Parsing: When a request arrives, the module inspects the CDN-Loop header for an entry matching your configured CDN identifier. The header follows the format: CDN-Loop: my_cdn; loops=3, other_cdn; loops=1

  2. Tracking: The module extracts the current hop count for your CDN identifier. It then constructs an updated CDN-Loop header value with the count incremented by one.

  3. 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 return directive in NGINX short-circuits processing before the access phase. If you use return in a location block, automatic loop blocking will not trigger. Use proxy_pass or 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_module directive 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-Loop header 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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.