Apache 301 Redirect Loop: Diagnosing And Fixing Infinite Redirect Chains In VirtualHost And .htaccess Configurations
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+HTTP → non-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
| Scenario | Root Cause | Fix |
|---|---|---|
| HTTPS redirect loop behind load balancer | LB sends HTTP to Apache, Apache redirects to HTTPS, repeat | Check X-Forwarded-Proto header before redirecting |
| www ↔ non-www loop | Both VirtualHosts redirect to each other | Ensure only one VirtualHost has the redirect; the other should not |
| .htaccess loops on every request | Missing RewriteCond guards | Add RewriteCond %{HTTPS} !=on before redirect rule |
| Trailing slash loop | Apache and app both managing trailing slashes | Set DirectorySlash Off, let application handle it |
| Reverse proxy loop | Backend redirects to URL that proxy re-intercepts | Use Header edit Location to rewrite backend redirect headers |
| VirtualHost + .htaccess conflict | Both have HTTPS redirect rules | Remove 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.
Was this article helpful?
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
Apache 413 Request Entity Too Large: Fixing File Upload Failures With LimitRequestBody And PHP Configuration
The 413 error means Apache is rejecting your upload before it even hits PHP. Here's exactly what to change and where. Two layers can block large uploads: 1...
Apache 500 Internal Server Error: Diagnosing .htaccess Syntax Failures And Module Conflicts Step By Step
If you've been running web infrastructure long enough, you know the 500 Internal Server Error is Apache's way of saying "something's broken, and I'm not go...
Apache 403 Forbidden Error After Directory Permissions Fix: Troubleshooting SELinux And File Contexts
You've just spent an hour wrestling with Apache file permissions, carefully setting ownership to `apache:apache` and permissions to `755` on your directori...
Fix Apache 'AH00558: Could not reliably determine server's FQDN'
Resolve the Apache AH00558 warning about not being able to determine the server's fully qualified domain name by setting ServerName correctly.
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.
Apache Mod_security WAF Rules For OWASP Top 10 Protection
If you're running Apache in production without a Web Application Firewall, you're essentially leaving your front door unlocked. mod_security is the industr...
More in Apache
View all →Apache Mod_status Real-Time Server Monitoring With Grafana And Prometheus Integration
Monitoring Apache without proper observability tooling is like flying blind. You might catch a problem after your users start complaining, but by then, you...
Apache Performance Tuning: MPM, KeepAlive, and Caching
Tune Apache performance by choosing the right MPM, configuring KeepAlive correctly, and enabling mod_cache for significant throughput gains.
Apache as a Reverse Proxy: mod_proxy Configuration Guide
Configure Apache as a reverse proxy using mod_proxy to route traffic to backend applications with load balancing and health checks.
Apache SSL/TLS Hardening: Get an A+ on SSL Labs
Harden Apache SSL/TLS configuration to achieve an A+ rating on Qualys SSL Labs with modern cipher suites and security headers.