Nginx

Nginx HTTP to HTTPS Redirect Loop Behind Load Balancer

An HTTP-to-HTTPS redirect loop behind a load balancer happens because the load balancer terminates SSL and forwards plain HTTP to Nginx internally — Nginx, seeing only HTTP, keeps redirecting to HTTPS, and the load balancer keeps forwarding that redirected HTTPS request back to Nginx as HTTP again, endlessly.

The Problem

Visiting the site through a load balancer (a cloud load balancer, an AWS ALB, or similar) results in:

ERR_TOO_MANY_REDIRECTS
This page isn't working - example.com redirected you too many times

Testing Nginx directly, bypassing the load balancer, might work fine — which is a strong clue that the load balancer's specific SSL-termination setup is the actual source of the loop, not Nginx's redirect logic in isolation.

Why It Happens

Many load balancers terminate SSL/TLS at their own layer — meaning the load balancer itself handles the HTTPS encryption with the client, then forwards the request to backend servers (Nginx, in this case) over plain HTTP internally, since that internal traffic is typically within a private, trusted network. Nginx, seeing only a plain HTTP request arrive (with no visibility into the fact that the original client connection was actually HTTPS), applies its own configured HTTP-to-HTTPS redirect rule. The load balancer receives this redirect, dutifully sends the client to the HTTPS URL, the client reconnects over HTTPS to the load balancer, which again terminates SSL and forwards plain HTTP to Nginx — repeating the exact same cycle indefinitely.

The Fix

The correct fix is telling Nginx to trust and read the header the load balancer sets indicating the original request was HTTPS, rather than making Nginx redirect based on its own (incomplete) view of the connection. Most load balancers set X-Forwarded-Proto for exactly this purpose:

server {
    listen 80;

    if ($http_x_forwarded_proto != "https") {
        return 301 https://$host$request_uri;
    }

    # rest of server config
}

This conditional only redirects if the load balancer's own header indicates the original request genuinely wasn't HTTPS — for requests the load balancer already received over HTTPS (and is now forwarding internally as HTTP with the header correctly set), Nginx no longer redirects, breaking the loop entirely.

An often cleaner alternative to an if block (Nginx configuration generally discourages if where avoidable, due to well-documented quirks in how it interacts with other directives) is using a map block instead:

map $http_x_forwarded_proto $should_redirect {
    default "";
    http     "yes";
}

server {
    listen 80;

    if ($should_redirect = "yes") {
        return 301 https://$host$request_uri;
    }
}

Confirm your specific load balancer is actually configured to set X-Forwarded-Proto at all, since this isn't universal or automatic in every load balancer configuration — check your specific provider's documentation and dashboard settings for this header specifically, since a load balancer not setting it produces exactly this loop regardless of how correctly Nginx itself is configured to read it.

If your load balancer already handles redirecting HTTP to HTTPS at its own layer (many do, as a built-in feature independent of anything the backend configures), the cleanest fix might be removing the redirect logic from Nginx entirely for this scenario, letting the load balancer be the single source of truth for that redirect decision rather than having two separate layers each trying to enforce it.

Still Not Working?

If X-Forwarded-Proto is correctly set and read, but the loop somehow persists, check whether the browser has cached an old redirect response from before the fix, or has HSTS actively enforcing HTTPS-only behavior for the domain independently of anything the server currently sends — clearing the browser's cache and HSTS state for the specific domain (available in most browsers' internal network settings pages) removes this as a variable, confirming whether the server-side fix genuinely resolved the loop or whether client-side caching is masking a fix that's already working correctly.