hreflang has no failure mode you can see. A broken annotation does not throw an error, does not produce a warning in Search Console, and does not change anything on the page. It just stops being used, and German users keep landing on the English URL.
Since Google retired the International Targeting report in Search Console, there is no first-party surface that tells you which of your annotations were accepted. You have to build that check yourself, and to build it you need to understand what Google is actually assembling when it reads an hreflang attribute.
hreflang is a cluster, not a tag
The thing Google builds from your annotations is a set of alternates — a group of URLs each labeled with a language and optional region. The attribute on any single page is not self-contained; it is one vote toward the existence of a group.
Google has documented that annotations must be confirmed bidirectionally: if page A declares B as its de-DE alternate, B must declare A in return. Without that return link, the documentation says the annotations may be ignored or interpreted incorrectly. The mechanism is anti-spam — without reciprocity, any site could claim to be the French alternate of a competitor.
This is why hreflang behaves so unlike other on-page signals. A rel=canonical on one page does something on its own. An hreflang attribute on one page does nothing on its own. The unit of success is the cluster, and a cluster with one broken edge can lose more than that one edge.
Why reciprocity breaks in practice
Reciprocity almost never breaks because someone forgot to add the tag. It breaks because the two sides of the pair disagree about the exact URL string.
- The English page points to
https://example.com/de/produkt/and the German page is served athttps://example.com/de/produktwithout the trailing slash. - One side uses
http://, orwww., or a staging hostname that survived a template deploy. - One side carries a tracking or language parameter the other does not.
- The German page 301-redirects to a new path and the English page still points at the old one. Google follows redirects for users but the annotation now names a URL that is not the indexable one.
hreflang matching is on the URL as a string, not on "whatever this resolves to." Treat any difference in scheme, host, case, trailing slash, or query as a different URL, because that is how it will be treated upstream.
Canonical conflicts delete annotations quietly
The second large failure class is a conflict between hreflang and canonicalization. Google's documentation is explicit that hreflang annotations should point to canonical URLs, and that each page's self-reference should match its own canonical.
The common accident: a regional page carries rel=canonical pointing at the global page — someone added it to "avoid duplicate content" between en-us and en-gb — while also carrying hreflang annotations. You have now told Google two incompatible things: this URL is not the one to index and this URL is the one to serve British users. Google's documented guidance resolves this in favor of the canonical, which means the regional URL drops out of the index and the cluster edge pointing at it has nowhere to land.
A softer version of the same problem happens without any explicit canonical tag. If your en-us and en-gb pages are near-identical, Google may cluster them on its own and pick one as canonical. The reasonable inference — not something Google has documented in detail — is that annotations naming the non-selected duplicate become much less useful, because the selected canonical is what actually competes in results. If your regional variants differ only in currency symbol and a phone number, expect this.
The failure modes worth checking, in order
| Failure | What you see in a crawl | Effect |
|---|---|---|
| Missing return link | A → B exists, B → A absent | Edge unconfirmed; likely ignored |
| URL string mismatch | Trailing slash, scheme, host differ | Same as missing return link |
| Target redirects | hreflang target returns 301/302 | Annotation names a non-canonical URL |
| Target is 404/410/noindex | Non-indexable alternate | Cluster member cannot be served |
| Canonical points elsewhere | Canonical ≠ self-referencing hreflang | Annotations likely discarded |
| Invalid code | en-uk, en_US, hreflang="us" | Entry ignored; region alone is invalid |
| Missing self-reference | Page lists siblings but not itself | Weakens cluster membership |
| Conflicting sources | Sitemap and <head> disagree | Unpredictable resolution |
On language codes: Google documents ISO 639-1 for language and optional ISO 3166-1 Alpha 2 for region. The United Kingdom is GB, not UK. A region without a language (hreflang="us") is not valid. Underscores are not valid separators.
Building the validation dataset
The check is a graph problem, and you already have the data if you crawl with hreflang extraction enabled. Export one row per annotation edge, then:
-- unconfirmed edges: A declares B, B never declares A
SELECT a.source_url, a.target_url, a.hreflang
FROM hreflang_edges a
LEFT JOIN hreflang_edges b
ON b.source_url = a.target_url
AND b.target_url = a.source_url
WHERE b.source_url IS NULL;
Then join target_url against your crawl's status and canonical columns to catch redirecting, non-indexable, and canonical-conflicting targets in the same pass. Count unconfirmed edges as a percentage of total edges and track it per release — it is a far better health metric than "do we have hreflang: yes/no."
If you implement in HTTP headers for non-HTML files, the syntax is:
Link: <https://example.com/en-gb/guide.pdf>; rel="alternate"; hreflang="en-GB",
<https://example.com/en-us/guide.pdf>; rel="alternate"; hreflang="en-US"
For large sites, XML sitemaps are usually the more reliable implementation, because the entire cluster is generated from one data structure and reciprocity is a property of the generator rather than of thirty templates.
Measuring whether it is actually working
hreflang is a serving signal, not a ranking signal — Google's position is that it helps select which of your URLs to show, not how high it shows. So do not measure it with total clicks. Measure it with the country-by-page crosstab.
Pull country and page together from the Search Console API or the BigQuery export and ask one question per cluster: for users in Germany, what share of impressions went to the de-DE URL versus its siblings? Baseline that share before a fix and re-measure after. Correct swapping shows up as a shift in which URL receives a given country's impressions, with roughly stable cluster-level totals. If the share barely moves, the annotations were probably never the constraint — check for an unconfirmed edge or a canonical conflict before assuming Google ignored you.
What to do
- Crawl with hreflang extraction on, build the edge list, and run the reciprocity join. Fix unconfirmed edges first; they are the largest category.
- Normalize URL generation to one function. Trailing slash, scheme, and host should be impossible to get wrong per-template.
- Assert that every page's self-referencing hreflang equals its own
rel=canonical. Fail the build if it does not. - Delete cross-regional canonicals that point at a global page while hreflang claims regional variants exist.
- Validate codes against ISO 639-1 and ISO 3166-1 Alpha 2 lists rather than eyeballing them, and add an
x-defaultfor your language selector or fallback page. - Store the country × page impression split per cluster monthly, so the next time someone asks whether hreflang is working, you have a before.