# ERR_TOO_MANY_REDIRECTS: Find and Break the Redirect Loop

> Fix ERR_TOO_MANY_REDIRECTS (redirected you too many times): Cloudflare Flexible SSL, WordPress URLs, nginx HTTPS loops and cookie loops, diagnosed with curl.

Source: https://howhttpworks.com/debug/err-too-many-redirects
Last reviewed: 2026-10-04

Error messages this page covers:
- `This page isn’t working. example.com redirected you too many times. Try deleting your cookies. ERR_TOO_MANY_REDIRECTS`
- `The page isn’t redirecting properly. Firefox has detected that the server is redirecting the request for this address in a way that will never complete. This problem can sometimes be caused by disabling or refusing to accept cookies.`
- `Safari Can’t Open the Page. Too many redirects occurred trying to open “https://example.com/”. This might occur if you open a page that is redirected to open another page which then is redirected to open the original page.`
- `curl: (47) Maximum (10) redirects followed`

> **TL;DR:** Two layers disagree about the canonical URL, so each redirects to a URL the other redirects away from. Run `curl -sIL --max-redirs 10 URL` and read who issues each `Location`. The most common cause is a CDN or load balancer talking plain HTTP to an origin that insists on HTTPS (Cloudflare Flexible SSL plus an origin redirect).

## What it means

The browser followed `Location` headers from one response to the next and gave up after 20 hops. Each response is a legitimate 3xx. Nothing is "down". A loop looks like this:

```http
GET https://example.com/ HTTP/2
< HTTP/2 301
< location: https://www.example.com/

GET https://www.example.com/ HTTP/2
< HTTP/2 301
< location: https://example.com/
```

The loop can span two URLs, as above, or many. It can also depend on state outside the URL: a cookie, a header added by a proxy, or `Accept-Language`. Chrome and Firefox give up after 20 redirects.

## Who sent it?

Run the diagnosis from a terminal, not the browser, so nothing is cached or hidden by the address bar:

```bash
curl -sSIL --max-redirs 10 https://example.com/ \
  | grep -iE '^(HTTP/|location:|server:|cf-ray:|via:|set-cookie:|x-powered-by:)'
```

Read the output top to bottom. For every hop, note the status, the `Location`, and who answered:

```text
HTTP/2 301
server: cloudflare
cf-ray: 8c1f2a3b4c5d6e7f-AMS
location: https://example.com/
HTTP/2 301
server: cloudflare
cf-ray: 8c1f2a3c5d6e7f80-AMS
location: https://example.com/
curl: (47) Maximum (10) redirects followed
```

`Location` pointing back to the URL you just requested is the signature of a loop that depends on a hidden input. In this example the same URL redirects to itself because the origin sees HTTP and thinks it must upgrade to HTTPS. Compare hops:

- `Server: cloudflare` plus a `CF-Ray` on a redirect with `Location` equal to the request URL: the CDN is relaying the origin's redirect, or a Cloudflare Redirect Rule or Page Rule is itself looping.
- `Server: nginx`, `Apache` or an app header such as `X-Powered-By: Express` on the redirect: the origin generated it.
- `Via: 1.1 ... (CloudFront)` or `Server: awselb/2.0`: a CloudFront behavior or an ALB redirect rule did it.

To check whether the loop depends on cookies, replay with a cookie jar. A loop that disappears with `-b/-c` is a cookie loop:

```bash
curl -sIL --max-redirs 10 -c jar.txt -b jar.txt https://example.com/account
```

To see exactly what the origin thinks it received, bypass the CDN and fake the forwarded scheme:

```bash
curl -sI http://ORIGIN_IP/ -H 'Host: example.com'
curl -sI http://ORIGIN_IP/ -H 'Host: example.com' -H 'X-Forwarded-Proto: https'
```

If the first returns a 301 to HTTPS and the second returns 200, the origin only needs to be told the original scheme.

## Fix it, in order of likelihood

1. HTTPS redirect behind a TLS-terminating proxy. The origin receives HTTP from a CDN or load balancer and redirects to HTTPS forever. Make the origin read `X-Forwarded-Proto` (or `Forwarded`), or serve HTTPS to the proxy.
2. www and apex redirecting at each other. Pick one canonical host and make sure the CDN, the web server and the application agree. A redirect in the DNS provider, one in the CDN and one in the app often do not match.
3. WordPress `siteurl` and `home` set to a different scheme or host than the one in the browser.
4. Cookie or session loop: login redirects to the app, the app does not see the session cookie, and redirects back to login.
5. Trailing slash or path normalization: one rule adds `/`, another strips it.
6. A redirect rule matching its own destination, for example a Cloudflare Redirect Rule for `http.host eq "example.com"` that redirects to `https://example.com/`, so it also matches the HTTPS request.

### Cloudflare Flexible SSL

In Flexible mode the visitor-to-Cloudflare leg is HTTPS and the Cloudflare-to-origin leg is HTTP. An origin with its own HTTP-to-HTTPS redirect (nginx `return 301`, Apache `RewriteRule`, WordPress, a plugin) will redirect every request Cloudflare makes, and the visitor ends up bouncing. The fix is in the Cloudflare dashboard under SSL/TLS, Overview: set the encryption mode to Full (strict) after installing a valid certificate (or a Cloudflare Origin CA certificate) on the origin. Full also stops the loop but does not validate the certificate. The modes are Off, Flexible, Full and Full (strict); Cloudflare's [troubleshooting page](https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/) also lists Always Use HTTPS, HSTS and conflicting Redirect Rules as loop sources. Do not "fix" it by removing the origin redirect while staying on Flexible; that leaves the Cloudflare-to-origin hop unencrypted.

### nginx

The common bug is a catch-all redirect that fires for traffic that already arrived over HTTPS via a proxy:

```nginx
# Loops behind a proxy that talks HTTP to nginx:
server {
    listen 80;
    return 301 https://$host$request_uri;
}
```

If nginx itself terminates TLS, redirect only on the plain-HTTP listener and keep the HTTPS server block redirect-free. If nginx sits behind a load balancer that terminates TLS, key the redirect on the forwarded scheme:

```nginx
server {
    listen 80;

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

    location / {
        proxy_pass http://app;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
    }
}
```

When nginx is the proxy in front of an application server that does its own HTTPS redirect, forward the scheme the client used:

```nginx
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
}
```

Then make the application trust that header:

```javascript
// Express
app.set('trust proxy', 1) // number of proxy hops you control; req.protocol now follows X-Forwarded-Proto
```

```python
# Django settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
```

Only trust `X-Forwarded-Proto` if your proxy overwrites it. A header supplied by the client must never decide security behavior.

### Apache

```apache
# Behind a proxy: look at the forwarded scheme, not %{HTTPS}
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```

`RewriteCond %{HTTPS} off` loops behind a TLS-terminating proxy because Apache always sees `off`.

### WordPress

WordPress redirects the request to whatever `siteurl` and `home` say. If either has the wrong scheme or host, every request is redirected. Override both in `wp-config.php` to break out of a loop you cannot reach through the admin:

```php
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');

// Behind a proxy or load balancer that terminates TLS:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}
```

Also disable "force SSL" options in security or caching plugins while testing; two plugins redirecting to HTTPS each is a classic.

### Cookie-based loops

Typical shape: `/account` redirects to `/login` when no session is found, and `/login` redirects to `/account` when it finds one. If the session cookie is set but never comes back, the app oscillates. Check in DevTools, Application, Cookies, whether the cookie is stored after the login response, and whether it is sent on the next request. Common reasons it is not:

- `Secure` on a cookie set over HTTP (the browser drops it), for example when TLS terminates at a proxy and the app thinks it is on HTTP.
- `SameSite=None` without `Secure`, which Chrome rejects.
- A `Domain` attribute that does not cover the host the user is on (`example.com` versus `www.example.com`).
- A cookie too large to send, so the server never sees it.

The [cookie not sent guide](https://howhttpworks.com/debug/cookie-not-sent) walks through each of these.

## Reproduce and verify

After the fix, the chain should end in a non-3xx within one or two hops:

```bash
curl -sIL --max-redirs 10 http://example.com/ | grep -iE '^(HTTP/|location:)'
```

```text
HTTP/1.1 301 Moved Permanently
location: https://example.com/
HTTP/2 200
```

Expected redirects are fine. Anything more than one hop from a typical entry URL (`http://www.` to `https://example.com/`) is worth collapsing into a single redirect, since each hop costs a round trip. Browsers cache 301 and 308 responses aggressively, so confirm in a private window rather than the tab that showed the error. To audit chains, canonical tags and indexing signals for a public URL, use the [redirect auditor](https://howhttpworks.com/tools/redirect-audit).

## Related

- [301 Moved Permanently](https://howhttpworks.com/status-codes/301) and [302 Found](https://howhttpworks.com/status-codes/302): permanent redirects get cached, temporary ones do not, which changes how long a loop survives.
- [308 Permanent Redirect](https://howhttpworks.com/status-codes/308) behaves like 301 but preserves the request method; a `POST` that loops through 308 never turns into a `GET`.
- [X-Forwarded-Proto](https://howhttpworks.com/headers/x-forwarded-proto) is the header most HTTPS-loop fixes depend on.
- [Location](https://howhttpworks.com/headers/location) explains how relative and absolute redirect targets are resolved.
