Troubleshooting

Fix ERR_TOO_MANY_REDIRECTS: diagnose a redirect loop

Trace an ERR_TOO_MANY_REDIRECTS loop across Nginx, Apache, WordPress and Cloudflare, and find the single rule that keeps sending traffic back to itself.

IntermediateLinux server7 min read

ERR_TOO_MANY_REDIRECTS means the browser asked for a page and got a 3xx response pointing somewhere else, followed that, got another redirect, and after roughly 20 hops gave up. The loop is almost never one broken rule — it is two rules that each think the other one is wrong about the protocol, the hostname or the trailing slash. This guide shows you how to read the redirect chain one hop at a time and then find the layer that closes the circle.

Before you start

You need:

  • SSH access to the VPS. If you are not connected yet, see Connect to your Linux VPS with SSH.
  • Knowledge of which web server serves the site (nginx, apache2 or httpd) and whether a proxy sits in front of it.
  • A way to test the headers. curl is on every supported distro; browsers hide the intermediate hops.

Install curl if it is missing:

Bash
# Ubuntu / Debian
sudo apt update && sudo apt install -y curl

# AlmaLinux / Rocky Linux
sudo dnf install -y curl

Before you change anything, note the current configuration files you plan to edit. A copy lets you roll back in seconds if a change locks you out of the site:

Bash
sudo cp /etc/nginx/sites-available/example.com /root/example.com.bak

Warning: Redirect rules sit in front of the whole site. A wrong return 301 or RewriteRule can take the site down for every visitor until you revert it. Keep the backup and test with curl before reloading the service.

Read the redirect chain

Do not guess. Ask the server what it actually returns, without following the hops, so you can see each Location: value in order.

Bash
curl -sSI http://example.com/

Expected output includes something like:

HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/

Now request that Location target and repeat until a hop points back to something you have already seen:

Bash
curl -sSI https://example.com/
curl -sSI https://www.example.com/

Use -I for a HEAD request and the extra -S so errors still print. If a hop returns 200, the chain ends there and the loop is coming from somewhere else — usually the application setting rather than the web server.

To see the whole chain at once and confirm the browser's behaviour:

Bash
curl -sSIL --max-redirs 10 http://example.com/ -o /dev/null -w '%{url_effective}\n'

The key questions are:

  • Does http:// go to https://, and does https:// stay on https://?
  • Does www.example.com go to example.com, and does example.com stay put?
  • Is a proxied request from Cloudflare or another proxy arriving as plain HTTP, so the "force HTTPS" rule fires again and again?

Tip: Write the hops down on paper (http://example.com → https://example.com → https://www.example.com → http://example.com). The repeated entry is your loop, and the rule that produced the second hop into it is the one to fix.

Check the web server rules

Nginx

List every place a redirect could be defined — site configs and the shared snippets:

Bash
sudo nginx -T | grep -nE 'return 30[12378]|rewrite|listen|server_name' | head -60

Look for two rules that disagree. A common pattern is a return 301 https://$host$request_uri; inside the port 80 block while a separate server block also redirects www to non-www, and the $host variable is expanded before the www rewrite, so the request bounces.

A safe, single-purpose HTTP-to-HTTPS block looks like this:

Nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

Note that the target is a literal hostname, not $host — mixing $host with a www-canonicalisation rule is the usual cause of the loop. If you proxy to an application, also confirm the app sees the original scheme:

Nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;

Without X-Forwarded-Proto, an application that forces HTTPS will redirect again even though the visitor is already on HTTPS. More patterns are in Nginx 301 redirects and rewrite rules explained and Redirect HTTP to HTTPS in Nginx.

Test and reload only after nginx -t passes:

Bash
sudo nginx -t && sudo systemctl reload nginx

Apache

Check the vhost and any .htaccess files in the document root and one level up:

Bash
sudo apachectl -S
sudo grep -rnE 'Redirect|RewriteRule|RewriteCond' /etc/apache2 /etc/httpd 2>/dev/null

Two rules that fight each other usually look like this:

Apache
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

If %{HTTPS} is off even on HTTPS requests (because TLS terminates at a proxy), this rule loops forever. Fix it by trusting the forwarded header:

Apache
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Then reload: sudo systemctl reload apache2 (Debian/Ubuntu) or sudo systemctl reload httpd (AlmaLinux/Rocky). More detail is in Apache redirects with.htaccess: HTTPS, www and 301s.

Check the application and the proxy

WordPress

WordPress stores its own idea of the site URL, and it will redirect to it no matter what your web server says. If siteurl or home is http://example.com while the web server forces HTTPS, you get a loop.

Bash
cd /var/www/example.com
wp option get siteurl --allow-root
wp option get home --allow-root

Set both to the canonical HTTPS URL, then flush rewrite rules:

Bash
wp option update siteurl 'https://example.com' --allow-root
wp option update home 'https://example.com' --allow-root
wp rewrite flush --allow-root

Also check wp-config.php for a hard-coded WP_HOME or WP_SITEURL that overrides the database, and look for a redirect plugin that duplicates the web server rule.

Cloudflare and other proxies

If the DNS record is proxied, Cloudflare can redirect HTTP to HTTPS on its own. When the origin also redirects, and the proxy talks to the origin over HTTP on port 80, the two fight. Pick one place for the redirect:

  • Set SSL/TLS mode to Full (strict) so the edge talks to your origin over HTTPS, or
  • Turn off the edge redirect and let Nginx or Apache do it.

Also confirm the proxy is not stripping or rewriting the Host header, which makes the origin send the visitor to a hostname that redirects back. Check Cloudflare's current documentation for the exact setting names, since they change.

If the site sits behind a CDN you cannot change, look at the origin access logs to see which scheme and host arrive:

Bash
sudo tail -n 50 /var/log/nginx/access.log
sudo tail -n 50 /var/log/apache2/access.log

Check it works

Run the chain again for each entry point and confirm it ends in a single 200 with no repeats:

Bash
for u in http://example.com/ http://www.example.com/ https://www.example.com/ https://example.com/; do
  echo "== $u"
  curl -sSIL --max-redirs 10 "$u" -o /dev/null -w 'final: %{url_effective} status: %{http_code} redirects: %{num_redirects}\n'
done

You want one or two redirects at most — for example http://www → https://example.com in a single hop — and a final 200. Then hard-reload in a private browser window to rule out cached 301 responses; browsers cache permanent redirects aggressively.

Troubleshooting

  • Loop only on www, fine on the apex domain → the www-to-non-www rule sends traffic to https://example.com while the HTTP-to-HTTPS rule uses $host and sends it back to https://www.example.com. Make the HTTPS target a fixed hostname. See Redirect www to non-www in Nginx.
  • Loop only behind Cloudflare or a load balancer → the origin sees HTTP and re-applies its HTTPS rule. Trust X-Forwarded-Proto in Nginx or Apache, or move the edge to Full (strict).
  • Loop starts after a WordPress migration → siteurl and home still hold the old scheme or hostname. Update both and flush rewrites.
  • curl -I returns 200 but the browser still loops → a cached 301 or a service worker is replaying the chain. Clear site data for the domain and retest in a private window.
  • Loop on a subdirectory or a specific page → an application-level redirect (login, trailing slash, language prefix) points at a URL that the web server then rewrites back. Check the access log for the repeated path and search the app config for it.

Next steps

Once redirects behave, tidy up the rest of the web stack:

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.