hreflang Implementation: Google Says the Three Methods Are Equivalent, So the Choice Is an Engineering One
· Royking Niba
Most hreflang advice argues about which delivery method Google prefers. Google has already answered that, in writing, and the answer is that it does not have a preference. Its documentation on localized versions states that the three methods are equivalent from its perspective and that you can choose whichever is most convenient. Once you take that sentence seriously, the implementation question stops being an SEO question and becomes an engineering one: which method ships the fewest bytes, breaks in the fewest ways, and still works when you add the ninth locale. Below is the documentation in Google’s own words, then the arithmetic that actually decides it.
What Google documents, in Google’s words
Quoted from Google Search Central’s guidance on telling Google about localized versions of your page, last updated 21 September 2026, checked today.
The three methods are equivalent from Google’s perspective and you can choose the method that’s the most convenient for your site.
On choosing between HTML tags, HTTP headers and sitemaps
Each language version must list itself as well as all other language versions.
On what belongs in every set of annotations
If page X links to page Y, page Y must link back to page X. If this is not the case for all pages that use hreflang annotations, those annotations may be ignored.
On reciprocity
The first code of the hreflang attribute is the language code (in ISO 639-1 format) followed by an optional second code that represents the region code (in ISO 3166-1 Alpha 2 format).
On code format
The reserved x-default value is used when no other language/region matches the user’s browser setting.
On x-default
Two of those five sentences are the ones that get broken in practice. The self-listing rule means a set of six locales carries seven annotations on every page, not six, once x-default is included. The reciprocity rule means the failure mode is not a weaker signal but no signal at all: Google’s wording is that the annotations “may be ignored”, which is a cliff rather than a slope. A half-implemented hreflang set is not half as good as a complete one.
Note also what the page does not say. It gives no guidance on choosing between the three methods beyond convenience, and it says nothing about how hreflang interacts with rel=canonical. Anyone who tells you Google ranks one delivery method above another is not quoting the documentation.
The three methods, and what each one can reach
| Method | Where the annotations live | Works on non-HTML files | Who downloads them | Needs a deploy to change |
|---|---|---|---|---|
| HTML link elements | The head of every page | No | Every visitor and every crawler, on every request | Yes, a template change |
| HTTP Link headers | The response headers of every URL | Yes, including PDFs | Every visitor and every crawler, on every request | Usually a server or CDN config change |
| XML sitemap | One or more sitemap files | Yes | Crawlers only | No, regenerate the sitemap |
| Three methods | Google: equivalent | Two of three | One of three costs nothing to humans |
The third column is the reason the header method exists at all. An hreflang annotation cannot be put in the head of a PDF, so a site with localised documentation or localised price lists in PDF form has only two options for those files, and only one of them is cheap.
The worked example: six locales, 5,000 page sets
This is the dataset that decided it on the last international build I worked on. Six locales plus x-default is seven annotations per page. 5,000 page sets across six locales is 30,000 pages, and 30,000 pages at seven annotations each is 210,000 annotation lines that have to exist somewhere and stay reciprocal.
Where they exist is the whole decision. Assume a link element of roughly 98 bytes once the hreflang value and a realistic absolute href are written out, and a sitemap annotation line of roughly 100 bytes plus about 120 bytes of url and loc wrapper per URL.
| Route | Weight per page or URL | Total weight that exists | Shipped to humans | Monthly transfer at 1.2m page views |
|---|---|---|---|---|
| HTML head elements | 7 x 98 = 686 bytes in every page | 20.6 MB across 30,000 pages | Yes, on every page load | 823.2 MB, about 0.82 GB |
| HTTP Link headers | Roughly the same payload, in the headers | Comparable | Yes, on every request | Comparable to the head route |
| XML sitemap | 120 + 7 x 100 = 820 bytes per URL entry | 24.6 MB in sitemap files | No, crawlers only | Effectively nil for visitors |
| Head route against sitemap route | Similar one-off size | About 33 times the monthly bytes |
The two routes cost almost exactly the same to store, 20.6 MB against 24.6 MB. They differ by a factor of about 33 in what they cost to serve, because the head route bills the annotation weight to every human page load and the sitemap route bills it once to a crawler. Nothing in that 823 MB is read by a single visitor. It is routing metadata for machines, delivered down the one channel humans also pay for.
The head-weight curve, which is the part that bites later
Annotation count grows with locale count, so the head route gets worse every time the business opens a market. The same 1.2 million monthly page views, at different locale counts:
| Locales | Annotations per page, with x-default | Bytes added to every page | Monthly transfer at 1.2m page views |
|---|---|---|---|
| 2 | 3 | 294 B | 0.35 GB |
| 5 | 6 | 588 B | 0.71 GB |
| 8 | 9 | 882 B | 1.06 GB |
| 12 | 13 | 1,274 B | 1.53 GB |
| 20 | 21 | 2,058 B | 2.47 GB |
| 30 | 31 | 3,038 B | 3.65 GB |
| 2 to 30 locales | 3 to 31 | Ten times the head weight | Ten times the transfer |
Where the sitemap route runs out of room
The sitemap route has its own ceiling and almost nobody plans for it. Google’s documentation on building a sitemap, last updated 8 July 2026, is explicit:
All formats limit a single sitemap to 50MB (uncompressed) or 50,000 URLs.
On sitemap size limits
If you have a larger file or more URLs, you must break your sitemap into multiple sitemaps. You can optionally create a sitemap index file and submit that single index file to Google.
On exceeding the limits
Those are two limits, not one, and which of them binds depends on how many locales you have. Every hreflang annotation you add to a sitemap entry makes the entry heavier without adding a URL, so past a certain locale count the 50 MB cap starts biting before the 50,000 URL cap does. Using the same 820-bytes-at-seven-annotations basis:
| Locales | Annotations per URL | Bytes per URL entry | URLs allowed by the 50 MB cap | Which limit binds |
|---|---|---|---|---|
| 2 | 3 | 420 B | 119,047 | The 50,000 URL cap |
| 5 | 6 | 720 B | 69,444 | The 50,000 URL cap |
| 8 | 9 | 1,020 B | 49,019 | The 50 MB byte cap |
| 10 | 11 | 1,220 B | 40,983 | The 50 MB byte cap |
| 12 | 13 | 1,420 B | 35,211 | The 50 MB byte cap |
| 20 | 21 | 2,220 B | 22,522 | The 50 MB byte cap |
| 30 | 31 | 3,220 B | 15,527 | The 50 MB byte cap |
| The crossover | 9 annotations | 1,020 B | About 49,000 | At roughly 8 locales |
At around eight locales the 50,000 URL limit stops being the number that matters. A sitemap generator that splits strictly on a 50,000 URL count, which is what most of them do, will quietly start emitting files over the uncompressed byte limit for any site with nine or more locales. At thirty locales the real ceiling is about 15,500 URLs a file, less than a third of the count-based split. That is a sitemap that validates on URL count and fails on size.
What these models assume
Both tables rest on assumed byte sizes: 98 bytes for a head link element, 100 bytes for a sitemap annotation line and 120 bytes of wrapper per URL entry. Real figures move with the length of your URLs, and a site with long localised slugs will be heavier than this and a site on short paths lighter. The monthly transfer figures also ignore gzip and Brotli, which will compress repetitive annotation blocks well, so treat 823 MB as the uncompressed upper bound rather than what crosses the wire. The 50 MB sitemap cap is explicitly an uncompressed limit, so compression does not rescue that one. Two conclusions survive all of the caveats: the head route is the only one that charges humans for machine metadata, and the byte cap overtakes the URL cap at around eight locales.
The reciprocity surface, which is what actually breaks
Google’s reciprocity rule is about pairs, and pairs grow with the square of the locale count. Six locales is six times five, so 30 ordered references per page set, 15 unordered pairs. Across 5,000 page sets that is 150,000 ordered references and 75,000 pairs that must all point both ways or the set may be discarded.
This is the structural argument for the sitemap route rather than the byte argument, and it is the stronger of the two. In the head-tag route those 150,000 references are generated by templates on 30,000 separately deployed pages, so a single locale whose template is behind by one release breaks reciprocity for every set it appears in. In the sitemap route all 150,000 live in files produced by one generator from one source of truth, which can be validated before publishing. You cannot diff 30,000 page heads in CI. You can diff a sitemap.
How I implement it
- Default to the XML sitemap route for HTML pages, on the byte argument and the single-source-of-truth argument, not because Google prefers it. It does not.
- Use HTTP Link headers only for the files that cannot carry a head element, which in practice means PDFs and other documents.
- Split sitemaps on bytes as well as URL count, because past about eight locales the 50 MB uncompressed cap binds first and most generators only count URLs.
- Validate reciprocity as a build step: every URL in the set must appear in every other URL’s annotation list, including its own. Google’s wording is that non-reciprocal annotations may be ignored entirely, so this is a pass or fail check rather than a score.
- Check the code format against the two standards the documentation names, ISO 639-1 for language and ISO 3166-1 Alpha 2 for region, rather than against whatever the CMS locale picker produced.
- Include x-default once per set, and make sure the page it points at is the one you actually want a non-matching browser to land on.
One thing worth separating from all of the above: none of this makes a page rank. hreflang decides which version of a page a matching user is shown, not whether the set ranks at all. The structural decision about where localised versions live, which is a different and larger question, is the one that moves traffic.
Related reading
- Google Penalty Recovery: how I diagnose and reverse a traffic collapse
- International SEO: the URL structure decision, and the hreflang arithmetic behind it
- Canonical tag SEO: why Google ignores yours, and what to do about it
- X-Robots-Tag: the header that controls the 85 percent of a site a robots meta tag cannot reach
- Orphan pages: why Google never finds them, and the audit that surfaces yours
- Server-side rendering for SEO: what Google recommends now that dynamic rendering is a workaround
Leave a Reply