DevOpsil

Apache 301 Redirect Loop: Diagnosing And Fixing Infinite Redirect Chains In VirtualHost And .htaccess Configurations

Riku TanakaRiku Tanaka12 min read

Apache 301 Redirect Loop: Diagnosing and Fixing Infinite Redirect Chains in VirtualHost and .htaccess Configurations

Few things are more frustrating than deploying what you think is a clean redirect configuration, only to have your browser throw an ERR_TOO_MANY_REDIRECTS error. Worse, it happens in production, your monitoring starts screaming, and you're staring at Apache configs trying to figure out why you've created an infinite loop that bounces requests back and forth until the browser gives up.

I've been paged at 3 AM over this. Multiple times. Let me save you that pain.

This guide is the definitive reference for diagnosing, understanding, and fixing Apache redirect loops. We'll cover VirtualHost configurations, .htaccess rules, HTTPS enforcement gone wrong, reverse proxy scenarios, and give you a repeatable diagnostic process you can run every single time.


What Actually Happens in a Redirect Loop

Before we fix anything, let's understand the mechanics. A redirect loop occurs when Apache responds to a request with a 3xx redirect, and the destination of that redirect triggers another redirect — either back to the original URL or into a chain that never terminates.

From an observability standpoint, here's what you'll see:

Client → Apache (301 → B)
B → Apache (301 → A)
A → Apache (301 → B)
... [repeat until browser aborts]

Browsers typically allow 20–30 redirect hops before throwing the error. Your access logs will show a burst of 301 responses in rapid succession from the same IP.

Here's a quick log pattern to confirm a loop is happening:

# Check for redirect storm patterns in access log
grep "301" /var/log/apache2/access.log | \
  awk '{print $1, $7}' | \
  sort | uniq -c | sort -rn | head -20

If you see the same URL combination repeating with high counts, you've confirmed the loop. Now let's find out why.


The Most Common Causes

1. HTTPS Enforcement Without SSL Termination Awareness

This is the number one cause I see. Someone adds a redirect to force HTTPS, not realizing that SSL is being terminated by a load balancer or reverse proxy upstream. Apache sees plain HTTP traffic regardless, so it redirects forever.

# This looks innocent but will loop if SSL is terminated upstream
<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

If your load balancer sends traffic to Apache on port 80 (even for originally-HTTPS requests), and you're redirecting all port 80 traffic to HTTPS, you've created a loop.

2. RewriteRule Without Proper Conditions

The classic .htaccess mistake:

# WRONG - no conditions, will loop
RewriteEngine On
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]

This rewrites every request including the one Apache is already processing. The L flag stops the current ruleset, but the browser follows the redirect and Apache starts over.

3. Conflicting VirtualHost and .htaccess Rules

When you have redirects at both the VirtualHost level and in .htaccess, they can compound. Apache processes VirtualHost directives first, then .htaccess. If both have overlapping redirect logic, you get a chain.

4. www to non-www (or vice versa) Combined with HTTPS

# Both of these together can create a triangle loop
# Rule 1: Redirect www to non-www
# Rule 2: Redirect HTTP to HTTPS
# If conditions aren't mutually exclusive, requests can bounce

5. DocumentRoot Mismatch with Redirect Target

If your redirect points to a path that itself triggers another redirect due to directory indexes or trailing slashes, you get chained redirects.


Diagnostic Process: A Systematic Approach

Don't guess. Measure. Here's the exact process I follow.

Step 1: Use curl to Trace the Redirect Chain

Never use a browser for initial diagnosis — it hides too much. Use curl with verbose output:

# Follow redirects and show all headers
curl -vL --max-redirs 10 http://example.com/ 2>&1 | grep -E "(Location:|HTTP/|> GET)"

# Even better - show just the redirect chain
curl -sI -L --max-redirs 10 http://example.com/ 2>&1 | grep -E "(HTTP/|Location:)"

If you see the same URLs repeating in the output, you've confirmed and mapped the loop. The --max-redirs 10 prevents curl from looping indefinitely.

Step 2: Check Apache Error Logs

# Look for redirect-related errors
tail -f /var/log/apache2/error.log | grep -i "redirect\|rewrite\|loop"

# Check for RewriteRule processing issues
grep "rewrite" /var/log/apache2/error.log | tail -50

Step 3: Enable RewriteLog for Deep Inspection

For mod_rewrite issues specifically, enable the rewrite log temporarily:

# Apache 2.4 - add to VirtualHost or httpd.conf
LogLevel alert rewrite:trace8

Then check:

tail -f /var/log/apache2/error.log | grep "rewrite"

This gives you every single rewrite evaluation with condition matching. It's verbose, but it will show you exactly where your rules are firing and in what order. Disable this after debugging — it generates enormous log volume under any real traffic load.

Step 4: Test Each VirtualHost in Isolation

# Test specific VirtualHost by forcing Host header
curl -vI -H "Host: example.com" http://your-server-ip/

# Test HTTPS endpoint directly
curl -vI https://example.com/ --resolve example.com:443:your-server-ip

Step 5: Validate the X-Forwarded-Proto Header

If you're behind a load balancer, check whether it's setting the forwarded protocol header:

# Check what headers your upstream is actually sending
curl -vI http://example.com/ 2>&1 | grep -i "x-forwarded\|x-real\|forwarded"

Fixing the Most Common Scenarios

Fix 1: HTTPS Redirect Behind a Load Balancer or Reverse Proxy

This is the canonical fix. You must check the X-Forwarded-Proto header instead of (or in addition to) the server port:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    RewriteEngine On

    # CRITICAL: Check X-Forwarded-Proto to detect already-HTTPS requests
    # that are being proxied to us on port 80
    RewriteCond %{HTTP:X-Forwarded-Proto} =http [OR]
    RewriteCond %{HTTP:X-Forwarded-Proto} ^$
    RewriteCond %{HTTPS} !=on
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

The key here: if X-Forwarded-Proto is https, we skip the redirect entirely. If your load balancer uses a different header (some use X-Forwarded-SSL or Front-End-Https), adjust accordingly:

# For AWS ALB / CloudFront
RewriteCond %{HTTP:X-Forwarded-Proto} !https

# For some older Microsoft/IIS reverse proxies
RewriteCond %{HTTP:Front-End-Https} !on

Fix 2: Proper www to non-www with HTTPS

The order of operations matters enormously here. Handle both redirects in a single pass to avoid chains:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    # Single redirect handles both www removal AND HTTPS upgrade
    # Avoids the two-hop chain: www+http → non-www+http → non-www+https
    RewriteEngine On
    RewriteCond %{HTTP:X-Forwarded-Proto} !https
    RewriteCond %{HTTPS} !=on
    RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

    # If already HTTPS but still has www
    RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
    RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    # ... SSL config ...

    # Handle www redirect at HTTPS level too
    RewriteEngine On
    RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
    RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
</VirtualHost>

Fix 3: The Classic .htaccess RewriteRule Loop

The fix requires proper RewriteCond guards:

RewriteEngine On

# Guard 1: Don't redirect if already HTTPS
RewriteCond %{HTTPS} on [OR]
# Guard 2: Don't redirect if proxy says it's already HTTPS
RewriteCond %{HTTP:X-Forwarded-Proto} =https
RewriteRule ^ - [L]

# Now safe to redirect
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Or the more common pattern using a negative match:

RewriteEngine On

# Only redirect if NOT already HTTPS (direct or proxied)
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Fix 4: Preventing DocumentRoot Trailing Slash Loops

Sometimes the loop isn't about HTTPS at all — it's about directory handling:

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/html

    # Prevent Apache from adding trailing slash redirects that conflict
    # with your app's own routing
    DirectorySlash Off

    # Or more surgically, only apply to specific directories
    <Directory /var/www/html>
        DirectorySlash Off
    </Directory>
</VirtualHost>

If you have an application (like a PHP app or Node.js proxy) that also does redirects for trailing slashes, and Apache is also doing it, you get a loop. Pick one layer to handle it.

Fix 5: Reverse Proxy Configuration Loops

When Apache is acting as a reverse proxy (proxying to another service), redirect loops often happen when the backend redirects back to a URL that Apache then proxies again:

<VirtualHost *:443>
    ServerName example.com

    ProxyPreserveHost On
    ProxyPass / http://localhost:3000/
    ProxyPassReverse / http://localhost:3000/

    # If backend redirects to http://example.com, ProxyPassReverse
    # should rewrite it. If it's not working, check the scheme:
    ProxyPassReverseCookieDomain localhost example.com
    ProxyPassReverseCookiePath / /

    # Handle backend redirecting to http:// when we're serving https://
    Header edit Location ^http://example\.com(.*) https://example.com$1
</VirtualHost>

The Header edit Location directive is a powerful safety net — it rewrites any Location headers in responses from the backend, preventing the browser from following a redirect back to HTTP.


Advanced: Diagnosing with Observability Tools

If you're running Apache in a monitored environment (and you should be), here are the signals to watch.

Prometheus + Apache Exporter Metrics

With apache_exporter, watch for these metrics during a redirect loop incident:

# High rate of 3xx responses is the smoking gun
rate(apache_accesses_total{handler="*", code=~"3.."}[5m])

# Requests per second - will spike during a loop storm
rate(apache_accesses_total[1m])

# If connections spike but requests don't complete, it's a loop
apache_connections{state="keepalive"}

Access Log Analysis with GoAccess or Similar

# Real-time analysis of redirect patterns
tail -f /var/log/apache2/access.log | \
  awk '$9 == 301 {print $7}' | \
  sort | uniq -c | sort -rn | head -10

SLO Impact Assessment

Redirect loops will crater your availability SLO. If you're tracking HTTP success rates, a loop produces 100% 3xx (or the eventual 4xx/5xx when clients give up), which means 0% 2xx. Your alerting should be firing.

If it's not, this is a good time to add an alert:

# Example Prometheus alerting rule
- alert: HighRedirectRate
  expr: |
    rate(apache_accesses_total{code=~"3.."}[5m]) /
    rate(apache_accesses_total[5m]) > 0.3
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Apache redirect rate above 30% - possible redirect loop"

Testing Your Fixes Before Deploying

Always validate config syntax first:

# Syntax check
apache2ctl configtest
# or
apachectl -t

# See the full parsed configuration (useful to spot unexpected includes)
apache2ctl -S

# Test with a dry-run of rewrite rules using mod_rewrite's trace
# Add to VirtualHost temporarily:
# LogLevel alert rewrite:trace3

Then validate with curl before touching a browser:

#!/bin/bash
# redirect_check.sh - Simple redirect chain validator
URL="${1:-http://example.com/}"
MAX_HOPS=10

echo "Tracing redirect chain for: $URL"
echo "---"

curl -sI \
  --max-redirs $MAX_HOPS \
  -w "\n--- Final Status: %{http_code} ---\n" \
  -L "$URL" 2>&1 | grep -E "(HTTP/|Location:|^---)"

Usage:

chmod +x redirect_check.sh
./redirect_check.sh http://www.example.com/

Best Practices: Opinionated Rules to Live By

After years of dealing with this, here are the rules I enforce on every Apache deployment.

1. Always check X-Forwarded-Proto in HTTPS redirect rules. Full stop. Even if you're not behind a proxy today, you might be tomorrow. The additional condition costs nothing.

2. Consolidate redirects to the minimum number of hops. www+HTTPnon-www+HTTPS should be a single 301, not two. Every redirect hop is latency, and chains are fragile.

3. Never put HTTPS redirect logic in both VirtualHost and .htaccess. Pick one. VirtualHost is preferred because it's evaluated before the filesystem and can't be overridden unexpectedly by a CMS or application deployment.

4. Use R=301 only when you're certain the redirect is permanent. During testing, use R=302 (temporary). Browsers cache 301s aggressively. If you deploy a broken 301, your users will get the error even after you fix Apache, until their browser cache expires.

# Use 302 during testing
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L]

# Switch to 301 only after verifying it's correct
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

5. Document every redirect rule with a comment explaining why it exists. Future-you (or the person on call at 3 AM) will thank present-you.

# Redirect HTTP to HTTPS. SSL terminated at AWS ALB (port 443→80).
# Must check X-Forwarded-Proto since Apache only sees port 80 traffic.
# Added: 2024-01-15, Ticket: OPS-1234
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

6. Test every redirect scenario in staging with curl, not a browser. Browsers cache, lie about headers, and follow redirects silently. curl -vI is your ground truth.

7. Monitor your 3xx rate as an SLO signal. A sudden spike in 3xx responses at a ratio greater than historical baseline is an alert-worthy event.


Quick Reference: Redirect Loop Scenarios and Fixes

ScenarioRoot CauseFix
HTTPS redirect loop behind load balancerLB sends HTTP to Apache, Apache redirects to HTTPS, repeatCheck X-Forwarded-Proto header before redirecting
www ↔ non-www loopBoth VirtualHosts redirect to each otherEnsure only one VirtualHost has the redirect; the other should not
.htaccess loops on every requestMissing RewriteCond guardsAdd RewriteCond %{HTTPS} !=on before redirect rule
Trailing slash loopApache and app both managing trailing slashesSet DirectorySlash Off, let application handle it
Reverse proxy loopBackend redirects to URL that proxy re-interceptsUse Header edit Location to rewrite backend redirect headers
VirtualHost + .htaccess conflictBoth have HTTPS redirect rulesRemove from .htaccess, keep only in VirtualHost

Wrapping Up

Redirect loops are a configuration problem, which means they're entirely preventable with the right discipline. The pattern I see fail the most is deploying HTTPS redirects without accounting for SSL termination upstream — it's a remarkably easy mistake to make and an infuriatingly hard one to debug at 2 AM if you don't know what to look for.

The diagnostic process is always the same: use curl to trace the chain, check access logs for the burst pattern, enable rewrite trace logging if needed, and isolate whether the loop is in VirtualHost config, .htaccess, or both.

The fixes almost always come down to proper RewriteCond guards — making sure your redirect rules fire only when they actually need to, not on every single request regardless of context.

Measure first, fix second. And add that redirect rate metric to your dashboards before the next deployment.

Share:

Was this article helpful?

Riku Tanaka
Riku Tanaka

SRE & Observability Engineer

If it's not measured, it doesn't exist. SLO-driven, metrics-obsessed, and the person who gets paged at 3 AM so you don't have to. Observability isn't optional.

Related Articles

ApacheQuick RefBeginnerNeeds Review

Fix Apache mod_rewrite Not Working

Diagnose and fix Apache mod_rewrite rules not being applied, covering module loading, AllowOverride settings, .htaccess issues, and rewrite logging.

Aareez Asif·
3 min read

More in Apache

View all →

Discussion