Redirect loop
What is this issue?
A redirect loop is a redirect that leads back to an address already in its own chain, so the request never arrives anywhere:
/pricing → 301 → /pricing/
/pricing/ → 301 → /pricing
/pricing → 301 → …Browsers give up after a fixed number of hops and show "This page isn't working — redirected you too many times". Search engines give up the same way. The page behind the loop cannot be reached by anyone.
For a page to pass this check:
- No redirect PixyScan follows comes back to an address it has already visited in the same chain, and no page fails to load because its redirects never end.
Example: a rule that adds a trailing slash and another that removes it, both active on the same path. Each undoes the other, forever.
How this differs from "Redirect chains and loops" (#10). #10 now reports chains — redirects that take more than one hop but do arrive. A chain costs time and crawl budget; a loop leaves the page unreachable. A URL that loops is reported here only, never under #10 as well.
Why it matters
The page is unreachable. Every visitor who follows a link to it gets a browser error. Whatever the page was for — a product, a sign-up, an article — it is gone for as long as the loop exists.
Search engines drop it. Googlebot follows up to ten redirect hops and then reports a redirect error for the URL; the page cannot be indexed, and any ranking it had is lost.
Every link to it is wasted. Internal links, sitemap entries and backlinks pointing into the loop pass nothing on, because nothing at the end receives it.
It is usually one configuration mistake. Two rules that disagree (http↔https, www↔non-www, slash↔no-slash), a CMS redirect fighting a server redirect, or a cookie check that redirects to a page that redirects back. One fix usually clears every URL affected.
Effect on the health score
This is an important issue under the Link Integrity lens.
How to fix it
Trace the loop. Run
curl -sIL --max-redirs 10 https://example.com/pricingand read thelocationheaders. The point where an address repeats is the loop.Find the two rules that disagree. Almost every loop is two rules undoing each other:
http → httpsat the CDN andhttps → httpat the origin (often a proxy that terminates TLS and an app that thinks it is serving plain HTTP — setX-Forwarded-Protohandling or the app's "behind a proxy" setting);www → non-wwwin one place andnon-www → wwwin another;- "add a trailing slash" and "remove the trailing slash";
- a CMS redirect and a server redirect on the same path.
Keep one rule and delete the other. Decide the canonical form (https, one host, one slash convention) and make every rule agree with it.
Check login and consent redirects. A page that redirects to
/login, which redirects back because a cookie was not set, loops for any client that does not keep cookies — including search engines. Public pages must not depend on a cookie to load.Re-scan and confirm the page answers
200(or a single301to a page that does).
## Before: both blocks active, each undoing the other
location = /pricing { return 301 /pricing/; }
location = /pricing/ { return 301 /pricing; }
## After: one canonical form
location = /pricing { return 301 /pricing/; }Examples
Example 1: Trailing-slash rules fighting
Fails:
GET /pricing → 301 Location: /pricing/
GET /pricing/ → 301 Location: /pricing
GET /pricing → 301 …Reported here against /pricing, with the recorded chain and hasLoop: true.
Passes: one rule, one direction.
GET /pricing → 301 Location: /pricing/
GET /pricing/ → 200 OKExample 2: A loop the crawler could only see as a failure
Fails: the CDN upgrades to https, the origin (behind a TLS-terminating proxy) believes it is on http and redirects to https again — through the CDN, which forwards it as http.
The request never completes. The page is recorded as failed with the reason
too_many_redirects and is reported here, with no chain attached.
Example 3: A chain — reported elsewhere
GET /old-pricing → 301 → /pricing → 301 → /pricing/ → 200 OKNot reported here. The redirects arrive. This is a redirect chain (#10): point
/old-pricing straight at /pricing/.
How PixyScan detects this
PixyScan reports a loop from two kinds of evidence.
A recorded redirect chain that repeats an address. When the crawler follows a redirect, it records every hop. If an address appears twice in the chain — or the chain ends on an address it already passed through — the chain is marked as a loop. After the crawl, every such chain is raised here, with the hops it recorded. A chain that loops is raised only here, not also as a redirect chain (#10).
A page that failed to load because its redirects never ended. A true infinite loop never produces a response: the crawler's HTTP client stops after its maximum number of hops and the request fails. PixyScan records why each failed request failed, and a page whose failure was "too many redirects" is raised here too. There is no chain to show in that case, because the client stopped before handing the hops back; the finding says what was observed.
One finding per page. A page found looping by both routes is raised once.
What is not reported here.
- A redirect chain of two or more hops that does arrive — that is #10.
- A single redirect with the wrong status code — that is #7.
- A redirect that arrives at a 4xx page — that is #121.
"Too many redirects" is read as a loop. Strictly it means "more hops than the client allows". No real site needs that many hops, and the page is unreachable either way, so it is reported as the loop it almost always is.
Respects your lens settings. Nothing is raised on a scan with the Link Integrity lens switched off.
What we store
Storage Level
Page Level
Database Table / Prisma Model
RedirectChain (redirect_chains) for loops the crawler recorded hop by hop; Url
(urls) for loops that made the page fail to load. The finding is stored on
audit_issues.details.
Fields Used — RedirectChain
| Field | Type | Description |
|---|---|---|
| redirect_chains.original_url | String | The address the crawler requested |
| redirect_chains.final_url | String | Where the chain ended when it ended |
| redirect_chains.chain | Json | Every hop, [{ url, statusCode }] |
| redirect_chains.chain_length | Int | Number of hops |
| redirect_chains.has_loop | Boolean | True when an address repeats in the chain. The deciding field |
Fields Used — Url
| Field | Type | Description |
|---|---|---|
| urls.fetch_error_class | String | Why the page could not be fetched. too_many_redirects raises this check |
| urls.fetch_error_message | String | The client's own error text, kept on the finding |
Stored Fields on the finding
| Field | Type | Description |
|---|---|---|
| message | String | What was observed |
| hasLoop | Boolean | Always true |
| originalUrl | String | The looping address |
| finalUrl | String | Where the recorded chain stopped (recorded loops only) |
| chainLength | Int | Hops recorded (recorded loops only) |
| chain | Json | The recorded hops, or null for a loop seen only as a failed fetch |
| fetchErrorClass | String | too_many_redirects (failed-fetch loops only) |
| fetchErrorMessage | String | The client's error text (failed-fetch loops only) |
Detection Dependencies
- HTTP Response — the redirect hops the crawler followed
- Crawl failures — the classified reason a request failed