Debugging Hreflang Errors in Search Console

This guide belongs to Hreflang Tag Generation and turns vague hreflang error reports into specific fixes. It explains how to read search engine reports, how to reproduce the problems yourself with a small cluster crawler, and how each common class of error maps to a cause in a headless stack: routing, caching, fallback handling or content.

Hreflang errors are hard to debug from the reports alone. They lag behind by days or weeks, they show samples rather than complete lists, and the same symptom, such as “no return tags”, can come from several different causes. A reliable workflow is to treat the reports as a hint about where to look, reproduce the issue with your own crawler on the current site, and fix the cause in the system that generates annotations, not by editing individual pages.

From report to root causeA search engine report of hreflang errors is sampled; a cluster crawler fetches each sample URL and all URLs in its cluster, checks reciprocity, status, canonicals and codes, and classifies each problem into routing, caching, fallback or content causes, each with its own fix.Search consoleerror samplesCluster crawlerfetch all membersChecksreciprocity, status,canonical, codesRoutingCachingFallbackContent
Reproduce first, then fix the generator; never patch individual pages.

The Problem

A media company’s search console showed several thousand “no return tags” errors for its Spanish and Portuguese sections, rising each week. The SEO team fixed a few pages by hand, which changed nothing. Engineering found no bug in the hreflang template, which looked correct when tested locally. The cause turned out to be caching: the Spanish and Portuguese translations were usually published days after the English articles, and the English pages stayed cached with clusters that did not yet include the new translations.

Reading the Reports

Search console’s international targeting and page indexing reports group hreflang issues into a few kinds. No return tags means a page lists an alternate that does not list it back. Unknown language code means an invalid code such as en-UK. Alternate page with proper canonical in the indexing report often means fallback pages in clusters, whose canonical points to the source. Not found or redirect errors for alternates mean URLs in clusters that return 404 or redirect. Each report shows sample URLs; export them, since they are the starting points for reproduction.

Note the dates. Reports reflect crawls from days or weeks ago; a problem fixed yesterday will keep appearing for a while. Always reproduce on the current site before assuming a report reflects the present state.

Error classes and their usual causes in headless stacksFor each common hreflang error class, what it means and the usual cause in a headless site, with where to fix it.ErrorMeaningUsual causeFix inNo return tagsA lists B, B does not list Astale cache on one membercluster cache tagsUnknown codeinvalid language or regiontypo in code mapconfiguration + CI checkAlternate is 404 or redirectURL in cluster not liveguessed paths, renamed slugsmanifest-based URLsAlternate with other canonicalmember not self-canonicalfallback page in clustermembership rulesConflicting annotationshead and sitemap differtwo implementationssingle builder
Most errors trace back to one of four systems, not to individual pages.

Reproducing with a Cluster Crawler

A cluster crawler takes a starting URL, reads its hreflang set, fetches every URL in that set, reads theirs, and compares. It also checks each member’s status code, canonical and code validity. The script below does this for a list of URLs, such as the samples exported from search console.

TypeScript
// scripts/check-hreflang.ts: node scripts/check-hreflang.ts urls.txt
import { readFileSync } from "node:fs";
import { parse } from "node-html-parser";

type Cluster = Map<string, string>; // hreflang -> href
const VALID = /^([a-z]{2,3})(-([A-Za-z]{4}|[A-Z]{2}))?$|^x-default$/;

async function readPage(url: string) {
  const res = await fetch(url, { redirect: "manual", headers: { "User-Agent": "hreflang-check" } });
  if (res.status !== 200) return { status: res.status, canonical: null, cluster: new Map() as Cluster };
  const doc = parse(await res.text());
  const cluster: Cluster = new Map();
  for (const l of doc.querySelectorAll('link[rel="alternate"][hreflang]')) cluster.set(l.getAttribute("hreflang")!, l.getAttribute("href")!);
  return { status: 200, canonical: doc.querySelector('link[rel="canonical"]')?.getAttribute("href") ?? null, cluster };
}

async function check(start: string) {
  const problems: string[] = [];
  const first = await readPage(start);
  for (const code of first.cluster.keys()) if (!VALID.test(code)) problems.push(`invalid code ${code}`);
  const members = [...new Set(first.cluster.values())];
  const pages = new Map(await Promise.all(members.map(async (u) => [u, await readPage(u)] as const)));
  for (const [url, page] of pages) {
    if (page.status !== 200) { problems.push(`${url} returns ${page.status}`); continue; }
    if (page.canonical !== url) problems.push(`${url} canonical is ${page.canonical}`);
    const same = page.cluster.size === first.cluster.size && [...first.cluster].every(([k, v]) => page.cluster.get(k) === v);
    if (!same) problems.push(`${url} has a different cluster (${page.cluster.size} vs ${first.cluster.size} entries)`);
  }
  return problems;
}

for (const url of readFileSync(process.argv[2], "utf8").split("\n").filter(Boolean)) {
  const problems = await check(url);
  console.log(problems.length ? `✗ ${url}\n  ${problems.join("\n  ")}` : `✓ ${url}`);
}

Run it against the report’s samples first. If the current site passes, the report is stale and the issue was already fixed. If it fails, the problem descriptions usually point directly at the cause: a member with a different cluster suggests caching, a 404 member suggests routing, a foreign canonical suggests fallback handling. To separate caching from generation, run the crawler twice, once through the CDN and once against the origin with caches bypassed; if the origin is consistent and the CDN is not, the cause is invalidation.

Tracing Causes and Fixing Them

Caching. Members rendered at different times with different clusters. Tag every member with a cluster tag and revalidate it on every publish, unpublish and slug change of any locale, as described in building reciprocal clusters. Regenerate the sitemap chunks for all members too.

Routing. Alternates that 404 or redirect come from paths built by string manipulation or from stale manifests after slug changes. Build alternate URLs from the route manifest and update it before revalidating pages.

Fallback handling. Members whose canonical points elsewhere are fallback or noindex pages that should not be members. Fix membership rules.

Codes. Invalid codes come from configuration. Validate codes in CI and at startup, as described in regional variants and x-default.

Duplicate implementations. If page heads and sitemaps disagree, remove one implementation or make both call the same builder.

Configuration Reference

Check Pass condition Failure usually means
Member status 200, no redirect Routing or unpublished member
Member canonical equals its own URL Fallback or noindex member
Cluster equality identical on all members Caching or two implementations
Codes valid ISO language and region Configuration typo
Head versus sitemap same entries Duplicate implementations
Origin versus CDN same result Invalidation missing

Gotchas & Edge Cases

  • Crawling through redirects. Follow no redirects when checking members; a redirecting alternate is an error even if the target is correct.
  • Trailing slashes and case. https://example.com/de/seite and https://example.com/de/seite/ are different URLs. Compare exactly, the way search engines do.
  • Sampling bias. Search console samples are not exhaustive. After fixing a cause, crawl a broad sample per locale, not only the reported URLs.
  • Rate limits. Crawling many clusters quickly can trigger your own bot protection. Identify the crawler with a user agent and allowlist it.

Worked Example

The media company ran the cluster crawler against 500 exported samples. Through the CDN, 62 percent failed with different clusters on different members; against the origin, all passed. The cause was cache invalidation: translations published later did not revalidate the English pages. Adding cluster tags revalidated on every locale’s publish fixed it. The crawler, run nightly on a rotating sample, confirmed zero mismatches from the next day; search console’s “no return tags” count declined steadily over the following five weeks as pages were recrawled.

Sampled clusters failing the reciprocity checkShare of 500 sampled clusters failing the crawler's reciprocity check through the CDN and at the origin before the fix, and through the CDN after adding cluster cache tags.CDN, before62 % of sampled clustersOrigin, before0 % of sampled clustersCDN, after fix0 % of sampled clusters
Comparing CDN and origin pinpointed caching as the cause in one run.

Preventing Regressions

Once the site is clean, keep it that way with the same tools. Run the cluster crawler in CI against a preview deployment for a fixed set of URLs covering every locale and a few tricky clusters, and fail the build on any problem. Run it nightly in production on a rotating sample large enough to cover every section each week. Watch search console’s hreflang reports weekly per locale, knowing that they confirm or contradict your own checks with a delay. And make hreflang part of the checklist for changes to routing, slugs, caching and locales, since almost every regression in practice comes from a change in one of those areas rather than from content.

Rollout Checklist

  • Export error samples from search console by error class.
  • Reproduce with the cluster crawler, through the CDN and against the origin.
  • Classify each failure as routing, caching, fallback, codes or duplicate implementations.
  • Fix the generator or invalidation, never individual pages.
  • Crawl broadly after fixes and run nightly checks.
  • Expect reports to take weeks to reflect fixes.

Frequently Asked Questions

Why do errors keep appearing after a fix?

Reports reflect past crawls. If your crawler shows the current site is clean, the reported errors will decline as pages are recrawled.

Can third-party SEO crawlers do this?

Yes, most offer hreflang audits. A small script of your own is easier to run in CI, to schedule nightly and to point at origin versus CDN, which is often the fastest way to find the cause.

Should we fix pages in the CMS when a report lists them?

Only if the cause is content, such as a missing translation flag or a wrong slug. Most causes are in code or caching, and page-level edits will not stick.

Does the crawler need JavaScript rendering?

Not for server-rendered annotations, which is how they should be delivered. If your site only adds hreflang with client-side JavaScript, fixing that is the first step.

How many errors are acceptable?

Aim for zero in your own checks, every day. Search console may show a small, shifting residue from pages recrawled at unlucky moments.