← All posts

X-Robots-Tag: Indexation Control at the HTTP Layer

VisibilityIQ

A media company moved its entire PDF library — 4,000 whitepapers, research reports, and gated assets — behind a “noindex” policy by adding a meta robots tag to the HTML landing pages. Six months later the PDFs were still in Google’s index, ranking for queries the company specifically did not want them to rank for, because the meta tag controlled the landing pages and did nothing whatsoever to the PDF files themselves. A PDF has no <head>. There is no place in a binary document to put a meta robots tag. The only way to tell Google not to index a PDF is to send the directive in the HTTP response header that delivers the file — the X-Robots-Tag.

This is the gap the X-Robots-Tag fills, and it is a category of indexation control that meta robots simply cannot reach. The meta robots tag lives in HTML. The X-Robots-Tag lives in the HTTP response, which every resource has — HTML pages, PDFs, images, videos, JSON feeds, CSV downloads, and every other file your server delivers. Understanding when the header is the only option, how it interacts with meta directives, and where it gets injected is the difference between actually controlling indexation and believing you have when you have not.

The Resources Meta Robots Cannot Touch

A meta robots tag is markup. It exists inside the <head> of an HTML document, parsed by the browser or crawler as part of rendering the page. That confines its reach to documents that have a head to parse.

Everything else on your site is delivered as a non-HTML response, and for those resources the HTTP header is the sole control surface:

PDFs. The single most common case. Documentation libraries, research reports, manuals, and gated content frequently get indexed and rank, sometimes outranking the HTML page that should own the query. X-Robots-Tag: noindex on the PDF response is the only way to remove it.

Images and videos. Image search indexes the image file directly. If you have product photos, internal diagrams, or licensed media you do not want appearing in Google Images, the image response header is the control. X-Robots-Tag: noindex on an image removes it from image search; noimageindex on a containing page’s header prevents that page’s images from being indexed.

Data files. JSON feeds, XML files that are not sitemaps, CSV exports, and API responses that happen to be crawlable can all end up indexed. A /export/users.csv endpoint or a /api/products.json feed linked anywhere internally becomes a crawl target. The header keeps them out.

Generated and binary downloads. ZIP archives, spreadsheets, and any file your application streams to the browser carries an HTTP response and therefore can carry an X-Robots-Tag.

The directives available in X-Robots-Tag are the full set meta robots supports: noindex, nofollow, noarchive, nosnippet, noimageindex, notranslate, unavailable_after: [date], max-snippet, max-image-preview, and max-video-preview. You can also target specific crawlers by prefixing the user agent: X-Robots-Tag: googlebot: noindex, nofollow applies only to Googlebot, while a directive with no agent prefix applies to all. Multiple X-Robots-Tag headers in a single response are permitted and combine.

The unavailable_after directive is worth singling out because it has no clean meta-tag equivalent for non-HTML resources and it solves a real problem: time-limited content. A press release, an event page, or a limited-time offer that should drop out of the index on a known date can carry X-Robots-Tag: unavailable_after: 2026-12-31T23:59:59Z, and Google will stop indexing the URL after that timestamp without anyone needing to deploy a change on the date. For PDFs and other binaries, the header is the only place this directive can live. Similarly, nosnippet and max-snippet applied at the header level let you control how AI-driven and traditional snippets quote a resource without editing the resource itself — useful for documents you do not own the rendering of.

The Conflict Rule: Most Restrictive Wins

The interaction between X-Robots-Tag and meta robots is governed by one principle, and it is not “the header takes priority” or “the most recent wins.” Google collects every robots directive it can find for a URL — the HTTP header, the meta tag in the raw HTML, and any robots directive present in the rendered DOM — and applies the union, resolving conflicts in favor of the most restrictive value.

The practical consequences:

If the header says index and the meta tag says noindex, the page is noindexed. Noindex is more restrictive than index, so noindex wins.

If the header says nofollow and the meta tag says follow, links on the page are not followed. Nofollow wins.

If the header says noindex and the meta tag is absent entirely, the page is noindexed. Absence is not “index” — it just means that source contributes no directive.

There is no scenario where a less-restrictive directive overrides a more-restrictive one. This makes accidental over-restriction the dangerous failure mode. A page whose template correctly emits <meta name="robots" content="index, follow"> will still be deindexed if a CDN rule or a server config block injects X-Robots-Tag: noindex into the response. The page source looks perfect. View-source shows index, follow. The page silently drops out of the index because the header — invisible in the rendered markup — carries a noindex that wins the conflict.

This is why diagnosing indexation problems requires inspecting the actual HTTP response, not just the page source. curl -I https://example.com/page or the Network panel’s response headers, or Search Console’s URL Inspection (which reports the indexing directives it actually observed), are the tools that reveal a header-level noindex. A page that “looks indexable” in the browser can carry a deindexing header that no amount of looking at the HTML will surface.

CDN-Level and Server-Level Injection

The X-Robots-Tag is set in the HTTP response, which means it is injected wherever the response is constructed or passed through: the application, the web server, or the CDN edge. Each layer is a place to apply it and a place to accidentally misapply it.

Application layer. Frameworks let you set response headers programmatically. This is the right place for per-resource logic — noindex a specific PDF based on a database flag, apply unavailable_after to time-limited content, set noindex on filtered listing endpoints. The control is precise but lives in code.

Web server layer. Apache’s Header set X-Robots-Tag "noindex" inside a <Files> or <FilesMatch> block, or Nginx’s add_header X-Robots-Tag "noindex"; inside a location block, applies the header by URL pattern or file type. This is the canonical way to noindex all PDFs at once:

location ~* \.(pdf)$ {
  add_header X-Robots-Tag "noindex";
}

One FilesMatch or location rule covers thousands of files without touching a single document. This is also where the all-PDFs-noindex policy from the opening example should have been applied.

CDN edge layer. Cloudflare Workers, Fastly VCL, and similar edge platforms can add or rewrite the X-Robots-Tag before the response reaches the crawler. This is powerful for site-wide policy — noindex an entire staging hostname, strip indexing on a path prefix during a migration — and it is the layer where the most dangerous accidents happen, because an edge rule applies globally and is often configured far from the team that owns the page templates. A Worker that adds X-Robots-Tag: noindex to every response on a “to be safe” staging config, then gets promoted to production unchanged, deindexes the whole site. The header is set at the edge, invisible in origin source, and the deindexing looks like a Google penalty until someone inspects the actual response headers at the edge.

The edge layer is also where you apply X-Robots-Tag to resources you do not control at the origin — third-party-served assets, legacy systems whose templates you cannot edit. Injecting the header at the CDN is sometimes the only practical way to control indexation of an upstream you cannot modify.

The Robots.txt Trap That Defeats Every noindex

The most common reason an X-Robots-Tag noindex fails to deindex a page has nothing to do with the header itself. It is robots.txt.

A noindex directive — header or meta, it does not matter — only takes effect if the crawler fetches the URL, receives the response, and reads the directive. If robots.txt disallows the URL, Googlebot never makes the request. It never receives the response. It never sees the X-Robots-Tag. The noindex is never read, and the URL can sit in the index indefinitely as a bare URL with no snippet, discovered through links but never crawled to learn it should be removed.

This produces the exact opposite of the intended outcome. Someone wanting to remove a section from the index reasons “I’ll block it in robots.txt and noindex it, belt and suspenders” — and the suspenders cut the belt. The robots.txt block prevents the noindex from ever being seen, so the pages stay indexed as URL-only entries, which is worse than either control alone.

The correct sequence to deindex a resource:

  1. Ensure the URL is crawlable — not disallowed in robots.txt.
  2. Serve X-Robots-Tag: noindex (or meta robots noindex for HTML) on the response.
  3. Wait for recrawl. Google must fetch the URL, read the header, and drop it from the index. This can take days to weeks depending on crawl frequency.
  4. Only then, if you want to save crawl budget, add a robots.txt disallow — after confirming the pages have dropped out.

Reversing steps 2 and 4 is the trap. Block first and the noindex is invisible forever.

X-Robots-Tag is the indexation control that operates below the markup, and that is exactly what makes it hard to audit by eye — the directive lives in a response header that page source never reveals. A technical-SEO platform that crawls and records the full HTTP response for every resource, not just HTML pages, surfaces the cases that matter: a PDF or feed that should be noindexed but carries no header, an HTML page deindexed by a CDN-injected X-Robots-Tag: noindex that conflicts with its own correct meta tag, and the robots.txt-versus-noindex trap where a disallowed URL can never receive the noindex it needs. Pairing the observed header directives against the meta directives against the robots.txt rules — and flagging where the most-restrictive-wins union produces an outcome the site owner did not intend — turns an invisible-by-default control surface into a list of specific responses to fix.

Frequently asked questions

When should I use X-Robots-Tag instead of a meta robots tag?
Use the header whenever the resource isn't an HTML document — PDFs, images, videos, CSV exports, JSON or XML feeds, and any binary file have no <head> to hold a meta robots tag, so the HTTP header is the only way to control their indexation. Also use it when you need to control indexation at the server or CDN layer without editing page templates — for instance, applying noindex to an entire staging subdomain or to all URLs matching a pattern. For ordinary HTML pages where you can edit the markup, meta robots and X-Robots-Tag are equivalent in effect; pick whichever your stack makes easier to maintain consistently.
What happens when X-Robots-Tag and meta robots conflict?
Google combines all robots directives it finds for a URL and applies the most restrictive one. If the HTTP header says index but the meta tag says noindex, the page is noindexed — noindex wins because it's more restrictive. The directives are unioned, not prioritized by source: a noindex anywhere (header, meta, or rendered DOM) takes effect. The dangerous case is an unintended conflict where a CDN injects X-Robots-Tag: noindex globally while your templates emit index, silently deindexing pages whose markup looks correct. Because the header isn't visible in page source, this kind of conflict is invisible without inspecting the actual HTTP response.
Can Googlebot see an X-Robots-Tag noindex if the URL is blocked in robots.txt?
No, and this is the most common way noindex via header fails. A noindex directive — whether in a header or a meta tag — only works if the crawler is allowed to fetch the URL and read the response. If robots.txt disallows the URL, Googlebot never requests it, never receives the X-Robots-Tag header, and never learns it should be deindexed. The URL can then linger in the index as a bare URL without a snippet. To deindex a page you must allow crawling so the noindex is read; only after the page has been recrawled and dropped should you consider adding a robots.txt disallow.