Redirect Analysis & Validation
Issue No: 8
Category: Crawl Behaviour
Issue type: Issue
Priority: CRITICAL
Description
Redirects automatically forward users and crawlers from one URL to another. Misconfigured redirects — including chains, loops, broken destinations, wrong status codes, and client-side redirects — waste crawl budget, dilute link equity, and degrade user experience.
How do we capture it
- Crawl all known URLs: Seed the crawl queue from the sitemap and discovered internal links. Send HTTP
GETrequests to every URL with redirect-following disabled at the HTTP client layer, capturing the raw status code andLocationheader on the first response. - Follow redirects hop by hop: For each
3xxresponse, resolve theLocationheader against the current URL per RFC 3986 rules, then issue a newGETto that resolved URL. Repeat up to a fixed maximum hop limit (e.g. 10). Normalize each hop URL before comparison:- Lowercase scheme and host.
- Remove default ports (
:80for HTTP,:443for HTTPS). - Normalize dot segments in path.
- Preserve query string order as received.
- Detect redirect loops: After normalizing each hop URL, check whether it already appears earlier in the same chain. If a URL repeats, mark
has_loop = trueand stop traversal immediately. - Detect redirect chains: Count total hops before a terminal non-
3xxresponse. Flaghas_redirect_chain = truewhen hop count exceeds one (A → B → C or longer). - Stop traversal and record error when: the next
LocationURL is malformed, the request times out or a network error occurs, or the maximum hop limit is reached without a terminal response — store the reason inredirect_error_code(MALFORMED_LOCATION,TIMEOUT,MAX_HOPS_EXCEEDED). - Check destination URL validity: Send a final
GETto the terminal URL. Recordfinal_status_code,Content-Type, and canonical tag. - Check HTTP → HTTPS: For every URL discovered on
http://, verify a redirect tohttps://exists and that the HTTPS version returns200(not another redirect). - Detect client-side redirects: Parse raw HTML of fetched pages for
<meta http-equiv="refresh">tags. JavaScript execution is not performed; only statically available meta-refresh tags are captured. - Cross-reference with analytics/logs: Enrich
hit_countper redirect rule from server logs or analytics. Identify high-traffic URLs involved in chains, loops, or broken destinations as highest priority.
What to store
| Field | Type | Comment |
|---|---|---|
site_id | integer | Which site this redirect rule belongs to |
from_path | text | Origin path or URL pattern that triggers the redirect |
to_path | text | Destination path or full URL the redirect points to |
status_code | integer | HTTP redirect status: 301, 302, 307, or 308 |
is_active | boolean | Whether this redirect rule is currently active |
is_regex | boolean | Whether from_path is a regex pattern rather than a literal path |
hit_count | integer | Number of times this redirect has been triggered |
last_hit_at | timestamp | Timestamp of the most recent hit on this redirect rule |
created_at | timestamp | When this redirect rule was created |
updated_at | timestamp | When this redirect rule was last modified |
note | text | Internal comment explaining why this redirect exists |
origin_url | text | The original URL a user or crawler would request |
final_url | text | The final destination URL after all hops are followed |
hop_count | integer | Total number of hops from origin to final destination |
has_loop | boolean | Whether a redirect loop was detected in this chain |
final_status_code | integer | HTTP status code of the final destination URL |
chain_json | jsonb | Full ordered array of hop URLs and status codes |
detected_at | timestamp | When this chain was last observed during a crawl |
is_resolved | boolean | Whether this chain has been acknowledged/fixed |
chain_id | integer | Which chain this hop belongs to |
hop_index | integer | Position in the chain (0 = origin, 1 = first redirect, etc.) |
url | text | The URL at this step in the redirect chain |
hop_status_code | integer | HTTP status code returned at this hop |
redirect_type | text | Classification: 'http', 'meta_refresh', 'javascript' |
response_time_ms | integer | Time in milliseconds to receive a response at this hop |
redirect_error_code | text | Crawler-side error: TIMEOUT, MALFORMED_LOCATION, or MAX_HOPS_EXCEEDED |
checked_at | timestamptz | Timestamp when redirect validation was executed |
Condition for trigger
The following rules determine when a redirect issue is triggered based on the data queried from the database.
-
Trigger Rule 1: Redirect loop detected
- Target Field:
has_loop - Evaluation Logic:
has_loop = true - Severity: CRITICAL
- Diagnostic Message: "Redirect loop detected: This URL is unresolvable. Crawlers and users will receive an error."
- Target Field:
-
Trigger Rule 2: Chain of 3 or more hops
- Target Field:
hop_count - Evaluation Logic:
hop_count >= 3 - Severity: WARNING
- Diagnostic Message: "Redirect chain of 3+ hops detected. Excessive hops waste crawl budget and increase latency."
- Target Field:
-
Trigger Rule 3: Chain of exactly 2 hops
- Target Field:
hop_count - Evaluation Logic:
hop_count = 2 - Severity: SUGGESTION
- Diagnostic Message: "Redirect chain of exactly 2 hops detected. Consider flattening to a single direct redirect."
- Target Field:
-
Trigger Rule 4: Max hop limit exceeded
- Target Field:
redirect_error_code - Evaluation Logic:
redirect_error_code = 'MAX_HOPS_EXCEEDED' - Severity: WARNING
- Diagnostic Message: "Redirect traversal exceeded the allowed hop limit before reaching a final URL. Simplify redirect routing."
- Target Field:
-
Trigger Rule 5: Broken redirect destination
- Target Field:
final_status_code - Evaluation Logic:
final_status_code IN (404, 410, 500, 502, 503) - Severity: CRITICAL
- Diagnostic Message: "Broken redirect detected: The final destination URL is unreachable."
- Target Field:
-
Trigger Rule 5: Wrong redirect type for permanent move
- Target Field:
status_code - Evaluation Logic:
status_code = 302 AND hit_count > 0 - Severity: WARNING
- Diagnostic Message: "Long-lived 302 redirect detected. If permanent, use 301 to pass link equity."
- Target Field:
-
Trigger Rule 6: Meta refresh or JS redirect
- Target Field:
redirect_type - Evaluation Logic:
redirect_type IN ('meta_refresh', 'javascript') - Severity: WARNING
- Diagnostic Message: "Client-side redirect detected. Use server-side redirects for reliable crawler handling."
- Target Field:
-
Trigger Rule 7: HTTP not redirecting to HTTPS
- Target Field:
origin_url - Evaluation Logic:
origin_url LIKE 'http://%' AND final_url NOT LIKE 'https://%' - Severity: WARNING
- Diagnostic Message: "Insecure URL not redirecting to HTTPS."
- Target Field:
-
Trigger Rule 8: High-traffic broken redirect
- Target Field:
hit_count - Evaluation Logic:
hit_count > 100 AND final_status_code != 200 - Severity: CRITICAL
- Diagnostic Message: "Frequently accessed redirect pointing to a dead destination."
- Target Field:
-
Trigger Rule 9: Obsolete redirect
- Target Field:
hit_count - Evaluation Logic:
hit_count = 0 - Severity: SUGGESTION
- Diagnostic Message: "Redirect has zero hits in 90 days. Consider cleaning up if outdated."
- Target Field:
Sources
- MDN — HTTP Redirections: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
- Google — Redirects and SEO: https://developers.google.com/search/docs/crawling-indexing/301-redirects
- Google — HTTP Status Codes: https://developers.google.com/search/docs/crawling-indexing/http-network-errors
- Google Search Console: https://search.google.com/search-console
- Bing Webmaster — Redirects: https://www.bing.com/webmasters/help/redirects-url-management-17a3b60e
- RFC 9110 — HTTP Semantics (IETF): https://www.rfc-editor.org/rfc/rfc9110#section-15.4
- RFC 3986 — URI Generic Syntax (IETF): https://www.rfc-editor.org/rfc/rfc3986
- W3C — URL Specification: https://url.spec.whatwg.org/
- Moz — Redirects & SEO: https://moz.com/learn/seo/redirection
- Ahrefs — Redirect Chains: https://ahrefs.com/blog/redirect-chains/
- Screaming Frog — Redirect Auditing: https://www.screamingfrog.co.uk/seo-spider/redirects/
- Cloudflare — Understanding Redirects: https://developers.cloudflare.com/rules/url-forwarding/
Long description
A redirect is a server instruction that automatically forwards a request from one URL to a different URL. When a browser or crawler requests a URL that has a redirect configured, the server responds with a 3xx HTTP status code and a Location header pointing to the new URL. The client then makes a second request to the new URL.
Redirects serve many legitimate purposes: handling site migrations, consolidating duplicate content, enforcing HTTPS, managing seasonal or promotional pages, and preserving inbound links when URLs change. However, every redirect adds latency, complexity, and potential for misconfiguration — all of which have measurable SEO consequences.
Types and SEO Behaviour:
- 301 (Moved Permanently): Passes equity (~99%). Page has permanently moved to a new URL.
- 302 (Found/Temporary): Does not pass equity. Used for temporary redirects, tests, or maintenance.
- 307 (Temporary Redirect): Like 302 but preserves HTTP method.
- 308 (Permanent Redirect): Like 301 but preserves HTTP method.
- Meta Refresh: Client-side (HTML), partial/delayed equity pass. Discouraged fallback.
Common Scenarios:
- Redirect Chain (A → B → C): Each hop adds latency, wastes crawl budget, and dilutes link equity.
- Redirect Loop (A → B → A): Chain never terminates. Crawlers abandon the URL, resulting in complete indexation loss.
- Broken Redirect: Destination returns 4xx/5xx, wasting link equity pointed to the original URL.
- HTTP Not Redirecting to HTTPS: Results in security warnings, penalized rankings, and exposes user data.
- Wrong Type (302 instead of 301): Search engines continue to treat the old URL as canonical and fail to consolidate link equity to the final destination.
- Dynamic Redirect Systems: Systems that emit malformed or unstable
Locationheaders, or infinite redirect paths that only terminate due to client-side hop limits. - Conflicting Rewrite Rules: Separate redirect rules (e.g. HTTP→HTTPS and non-www→www implemented as two independent hops) stack into chains instead of being consolidated.
How to Fix
- Flatten redirect chains: For any chain A → B → C, update the origin to point directly to the final destination. Audit intermediate hops and update internal links.
- Break redirect loops: Trace the chain until a URL repeats. Remove or correct the rule creating the cycle (often conflicts between
.htaccess, CMS, or CDN rules). - Fix broken redirect destinations: If the destination was deleted, point to the most relevant live alternative (or homepage). If it moved, update the final URL. If it's a server error, fix the server.
- Enforce HTTPS sitewide: Configure a blanket HTTP → HTTPS redirect at the server or CDN level (e.g., using
nginxreturn 301 orApacheRewriteRule) instead of individually per URL. - Replace 302s with 301s for permanent moves: Audit active 302s. If they've been in place for over 30 days and the move is permanent, change to 301 to pass link equity.
- Replace meta refresh and JS redirects: Implement an HTTP 301 (or 302) at the server level, remove the client-side tag from the HTML, and verify the response header contains the Location directive.
- Clean up obsolete redirects: Standardize redirects using zero or near-zero hits over a 90-day period. Confirm they no longer receive traffic and remove them to optimize rule-set processing overhead.
- Update internal links: Point all internal links directly to the final destination URL instead of intermediate redirect hops to eliminate latency and preserve link equity.