Back to blog
Guide March 30, 2026

How to Fix Nginx 301 and 308 Redirect Loops (Trailing Slash, HTTPS & Proxy Forwarding)

PPingzo Infrastructure Team
Automate WhatsApp Alerts
Start Free ➔

When configuring reverse proxies, edge CDNs, Single Page Applications (SPAs), or SSL termination in Nginx, web browsers may abruptly halt with the following client-side error:

ERR_TOO_MANY_REDIRECTS

Inspecting network headers reveals an infinite loop of HTTP 301 Moved Permanently or 308 Permanent Redirect status codes.

In production infrastructure, redirect loops are rarely caused by a single broken configuration line. They occur when two or more architectural layers disagree on the canonical URL representation—such as scheme (http vs https), host (example.com vs www.example.com), internal port (:8080 vs :443), or trailing slash (/app vs /app/).

In this guide, we break down Nginx's implicit directory-slash mechanics, reverse proxy URI stripping rules, Cloudflare Flexible SSL loops, and forwarded protocol headers, providing deterministic CLI diagnostics and production-ready configuration fixes.


[!TIP] Proactive Redirect & Header Verification Before pushing reverse proxy or trailing slash changes to production, trace your redirect hops and verify HTTP headers instantly using our free HTTP Header Checker, HTTP Status Code Checker, and Ping Test Tool.


The Mathematical Model of Redirect Loops

In HTTP architecture, canonicalization can be represented as a state transition function $f(\text{URL}) \to \text{URL}'$. A URL is represented by a 5-tuple:

$$\text{URL} = \langle \text{scheme}, \text{host}, \text{port}, \text{path}, \text{query} \rangle$$

A healthy infrastructure achieves a fixed point in at most one state transition:

$$f(\text{URL}{\text{initial}}) = \text{URL}{\text{canonical}} \quad \text{where} \quad f(\text{URL}{\text{canonical}}) = \text{URL}{\text{canonical}} \implies \text{HTTP } 200 \text{ OK}$$

A redirect loop occurs when the transformation function contains a non-terminating cycle:

$$f(\text{URL}_A) = \text{URL}_B \quad \text{and} \quad f(\text{URL}_B) = \text{URL}_A$$

+-------------------+      301 Redirect (Append '/')      +--------------------+
|  /app (No Slash)  | ----------------------------------> |  /app/ (With Slash) |
|  Nginx Directory  | <---------------------------------- |  Upstream App      |
+-------------------+      301 Redirect (Strip '/')       +--------------------+

1. Root Cause: Nginx Implicit Directory Trailing Slashes

The most common internal redirect in Nginx is automatic directory canonicalization.

How Nginx Handles Directories

When a request arrives for /app:

  1. Nginx checks if a physical directory named /var/www/html/app/ exists on disk (via root or alias).
  2. If it is a directory and the requested URI lacks a trailing slash /, Nginx automatically issues a 301 redirect to /app/ to ensure relative links within the directory resolve correctly.

The Conflict with Application Routing

If your upstream backend framework (e.g., Express, Django, FastAPI) or API gateway is configured to strip trailing slashes (/app/ $\to$ /app), an infinite oscillation loop is created:

  1. Client requests GET /app.
  2. Nginx sees local directory app/ and returns 301 Location: /app/.
  3. Client requests GET /app/.
  4. Upstream application middleware rejects trailing slashes and returns 301 Location: /app.
  5. Browser hits ERR_TOO_MANY_REDIRECTS.

The Solution: Explicit Location Boundaries

To prevent Nginx from guessing based on filesystem state, declare explicit routing rules:

# Explicitly handle canonical slash redirect at the web server layer
location = /app {
    return 301 /app/;
}

# Proxy the canonical trailing slash route
location /app/ {
    proxy_pass http://backend_upstream;
}

2. proxy_pass Trailing Slash Semantics Matrix

A major source of routing loops and URI corruption is misunderstanding how the trailing slash on proxy_pass alters the request path forwarded to upstream servers.

Request: GET /api/v1/users

Case A: proxy_pass http://backend:8000;   --> Upstream receives: /api/v1/users (Raw URI preserved)
Case B: proxy_pass http://backend:8000/;  --> Upstream receives: /users        (Prefix /api/v1/ replaced by /)

Complete Behavioral Matrix

Location Blockproxy_pass DirectiveIncoming RequestUpstream ReceivesArchitectural Use Case
location /api/http://backend:8000;/api/users/api/usersUpstream expects the full /api/ prefix.
location /api/http://backend:8000/;/api/users/usersUpstream is mounted at root /.
location /apphttp://backend:8000;/app/login/app/loginRaw path passthrough without prefix mutation.
location /apphttp://backend:8000/;/app/login//login (Bug!)Mismatched slashes cause double-slash upstream errors.

[!IMPORTANT] Slash Parity Rule: If your location block contains a trailing slash (location /dir/), your proxy_pass URI replacement target should also contain a trailing slash (proxy_pass http://backend/).


3. Scheme Mismatches: Cloudflare & Load Balancers

The second major category of 301 redirect loops occurs at the edge-to-origin boundary.

[ Browser ] --( HTTPS:443 )--> [ Cloudflare / ALB ] --( HTTP:80 )--> [ Nginx Origin ]
                                                                             |
                                                            Checks: $scheme == 'http'
                                                                             |
                                                            Returns: 301 to HTTPS
                                                                             v
[ Browser ] <--( 301 Location: https://example.com )-------------------------+

The "Flexible SSL" Loop Mechanism

  1. The client connects to Cloudflare over HTTPS.
  2. Cloudflare connects to your Nginx origin over plain HTTP on port 80 (Standard Flexible SSL mode).
  3. Nginx sees incoming traffic on port 80 where $scheme == "http".
  4. Nginx executes return 301 https://$host$request_uri;.
  5. Cloudflare receives the 301, forwards it to the browser, and the browser issues another HTTPS request to Cloudflare.
  6. Cloudflare again queries Nginx over HTTP, repeating the cycle infinitely.

Solution A: Enable Full (Strict) SSL on Cloudflare

Switch Cloudflare encryption mode from Flexible to Full (Strict) so Cloudflare connects to your origin over HTTPS on port 443.

Solution B: Terminate HTTPS via $http_x_forwarded_proto

If Nginx sits behind AWS ALB, GCP Load Balancer, or Cloudflare and must listen on HTTP port 80, inspect the forwarded protocol header instead of $scheme:

# Map incoming X-Forwarded-Proto header to detect true client scheme
map $http_x_forwarded_proto $forwarded_scheme {
    default $scheme;
    https   https;
    http    http;
}

server {
    listen 80;
    server_name example.com;

    # Only redirect to HTTPS if the original client request was plain HTTP
    if ($forwarded_scheme = "http") {
        return 301 https://$host$request_uri;
    }

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $forwarded_scheme;

        proxy_pass http://app_cluster;
    }
}

4. Internal Directives: absolute_redirect & port_in_redirect

When Nginx generates automatic redirects (e.g., appending a trailing slash to a directory), it constructs an absolute URL by default:

HTTP/1.1 301 Moved Permanently
Location: http://example.com:8080/app/

In multi-tier infrastructure where Nginx runs on internal port 8080 behind an edge proxy listening on 443, this leaks internal ports and downgrades HTTPS to HTTP.

Production Directives in nginx.conf

server {
    listen 8080;
    server_name example.com;

    # Emit relative redirects (e.g., 'Location: /app/') instead of absolute URLs
    absolute_redirect off;

    # Do not append internal listening port (:8080) to generated redirects
    port_in_redirect off;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

5. Single Page Application (SPA) Fallback Loops

In React, Vue, Next.js, and Angular deployments, developers frequently configure catch-all fallback routing:

# Problematic SPA Configuration
location / {
    try_files $uri $uri/ /index.html;
}

location = /index.html {
    return 301 /;  # <-- Creates instant redirect loop with try_files!
}

When a user visits /dashboard:

  1. try_files checks $uri (not found) and $uri/ (not found).
  2. It internally rewrites the request to /index.html.
  3. The location = /index.html block matches and issues 301 /.
  4. The client requests /, triggering the loop again.

Robust SPA Production Configuration

Use the internal directive to ensure the fallback template is never invoked as an external redirect target:

server {
    listen 443 ssl;
    server_name example.com;

    root /var/www/app/dist;
    index index.html;

    # Static assets cache directly
    location /assets/ {
        try_files $uri =404;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Internal fallback preventing external redirect oscillation
    location = /index.html {
        try_files $uri =404;
    }

    # SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }
}

6. CLI Diagnostics & Packet Capture Sequence

Follow this CLI hierarchy to trace redirect chains and inspect raw protocol headers.

1. Trace the Full Redirect Chain with curl

# Follow redirects and print headers for every hop
curl -ILv https://example.com/app

# Inspect the exact first Location header without following
curl -sSI https://example.com/app | grep -iE 'HTTP/|location:'

Example diagnostic trace:

HTTP/2 301 
location: https://example.com/app/

HTTP/2 301 
location: https://example.com/app

HTTP/2 301 
location: https://example.com/app/

This immediately confirms a trailing-slash canonicalization conflict.

2. Verify Full Expanded Configuration

Do not inspect isolated snippet files. Inspect the active, fully expanded runtime configuration:

sudo nginx -T | grep -nE 'return 30[18]|rewrite|proxy_pass|try_files|absolute_redirect|port_in_redirect'

3. Capture Origin HTTP GET Traffic with tcpdump

If debugging traffic arriving at your origin from an upstream load balancer or Cloudflare:

# Capture incoming HTTP GET requests on port 80
sudo tcpdump -nn -s0 -A 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420)'

Look specifically for incoming X-Forwarded-Proto and Host headers to verify what the origin server receives.


Redirect Root Cause & Remediation Summary

SymptomUnderlying CauseProduction Fix
/app $\leftrightarrow$ /app/ OscillationNginx directory check conflicts with app routingDefine explicit location = /app { return 301 /app/; } or set try_files $uri /index.html;.
HTTP $\leftrightarrow$ HTTPS LoopEdge proxy terminates TLS but connects to origin on port 80Inspect $http_x_forwarded_proto or enable Full/Strict SSL at CDN edge.
Port Leak (:8080 in redirect)Internal listening port included in absolute redirectAdd absolute_redirect off; and port_in_redirect off; in server block.
SPA Infinite 301 / Looptry_files fallback matches a redirecting /index.html blockRemove external redirect on index.html or mark fallback internal;.
Double Slash (//login) UpstreamTrailing slash missing on location but present on proxy_passEnforce slash parity between location /dir/ and proxy_pass http://backend/;.

Full-Stack Edge & Uptime Monitoring with Pingzo

Redirect loops cause immediate traffic drops, broken SEO indexing, and degraded user experiences. Synthetic monitoring from multiple geographic edges detects redirect loop anomalies and header misconfigurations instantly before users report downtime.

+-----------------------------------------------------------------------------+
|                         Pingzo Global Edge Network                          |
+-----------------------------------------------------------------------------+
               |                                   |
    [ Multi-Hop Redirect Audits ]         [ Real-Time SSL & Header Probes ]
               |                                   |
               v                                   v
+-----------------------------------------------------------------------------+
|               Your Web Server & Reverse Proxy Infrastructure                |
|           (Detects 301 Loops, 308 Loops, 502 Bad Gateway, 403 Errors)       |
+-----------------------------------------------------------------------------+
               |
               v
+-----------------------------------------------------------------------------+
|              Instant Multi-Channel Alerts: WhatsApp, Slack, & Telegram      |
+-----------------------------------------------------------------------------+

Pingzo Monitoring Plans

FeatureFreeStarter ($5/mo)Pro ($12/mo)Agency ($29/mo)
Check Frequency15 minutes5 minutes2 minutes1 minute
Monitors1 monitor10 monitorsUnlimitedUnlimited
Multi-Location TestingGlobalGlobalGlobalGlobal
SSL & DNS AuditsBasicAdvancedReal-TimeReal-Time + Custom Root
Alert ChannelsEmailEmail, Telegram, WhatsApp, Slack, DiscordAll Channels + Multi-RecipientWebhooks, SMS, White-Label Dashboards
Multi-Tenant ManagementNoNoNoYes (Multi-Team)

Detect redirect loops, protocol mismatches, and downtime instantly with Pingzo.

Zero-Code Uptime Alerts

Stop Finding Out About Outages from Angry Users

Get instant WhatsApp & Discord alerts the second your API, website, or server goes down. Setup in 30 seconds with 60-second checks.

WhatsApp & Discord 60-Second Checks Free Forever Plan
Try Pingzo Free

Know before your users do

Connect official WhatsApp notification channels, Discord webhooks, Telegram bots, and public status pages. Start in 30 seconds.

Create Free Monitor