Skip to content
Technical SEO10 min read

The Next.js caching bug that can serve Googlebot a blob of JavaScript instead of your page

We found this on our own site during a routine crawl: six URLs returning a React Server Component payload instead of HTML. Here is the diagnosis, the reproduction, and the fix.

Published by SearchLift SEO Consulting. Written under our editorial standards — no fabricated figures, and every claim checkable.

Key takeaways

  • Next.js prefetches return `text/x-component`, not HTML. If your CDN cache key ignores the `RSC` header, both share one cache entry.
  • Whichever response arrives first is then served to everyone — including Googlebot, which sees no title, no headings and no content.
  • It is intermittent by nature, so a single curl check will often show everything is fine.
  • The fix is a cache policy that varies on `RSC` and `Next-Router-Prefetch`. AWS CachingOptimized does not.

We found this on our own site, which is the only reason we can describe it precisely. A routine crawl reported that six pages had no title tag, no H1 and no viewport meta. Those pages demonstrably had all three. The crawler was not wrong — it had genuinely been served pages without them.

What Next.js is actually doing

In the App Router, every <Link> in the viewport prefetches its destination in the background so that clicking it feels instant. That prefetch is not a request for HTML. It asks for a React Server Component payload — a serialized description of the page the client router can apply directly.

Those requests are distinguished from ordinary navigations by request headers, and the response differs accordingly.

Two requests to the same URL, two entirely different responses.
RequestHeaders sentResponse content-type
A visitor typing the URL or Googlebot crawling itNone of the router headerstext/html
A <Link> prefetch from another pageRSC: 1, Next-Router-Prefetch: 1text/x-component

This is correct behaviour and nothing is wrong yet. The problem appears one layer out.

Where it breaks

A CDN decides what counts as "the same request" using a cache key. If the RSC header is not part of that key, then both requests in the table above are, as far as the CDN is concerned, identical. One cache entry serves both.

  1. 01

    A visitor lands on your homepage

    Next.js prefetches the links in view. Those prefetch requests reach the CDN carrying RSC: 1.

  2. 02

    The CDN has no entry for that URL yet

    It forwards to the origin, which correctly returns text/x-component because the request asked for it.

  3. 03

    The CDN caches that response

    Under a cache key that ignores the header that made the response different.

  4. 04

    Googlebot requests the same URL

    No RSC header, so it should get HTML. It gets the cached component payload: no <title>, no <h1>, no readable content.

The page is perfectly built, perfectly rendered, and perfectly indexable. It just was not the thing that got sent.

Why it is so easy to miss

When we first tried to reproduce it with curl, everything came back text/html. Every URL, repeatedly. It looked like the crawler had malfunctioned.

It had not. The poisoned entries were populated by the crawl itself — a real browser, following real links, firing real prefetches — and had since expired or been evicted. A tool that fetches URLs without executing JavaScript never generates the prefetch that causes the problem, so it never sees it.

  • It only appears after a real browser has prefetched that URL
  • It affects whoever requests the page next, then heals on its own
  • It is invisible in your browser, because your browser is the thing causing it
  • Different CDN edge locations hold different entries, so it varies by geography

How to check your own site in two minutes

Send a prefetch-shaped request, then immediately send a normal one to the same URL. If the second response is not HTML, you have this bug.

U="https://example.com/some-page?cb=$RANDOM"

# 1. Imitate a Next.js prefetch
curl -sI -H 'RSC: 1' -H 'Next-Router-Prefetch: 1' "$U" | grep -i content-type

# 2. Now request it the way a visitor or Googlebot would
curl -sI "$U" | grep -i content-type
Use a URL nothing has requested recently — the cache-busting parameter guarantees a cold entry.

The first response should be text/x-component. The second must be text/html. If the second one also returns text/x-component, the cache is serving the prefetch to everybody.

The fix

Add the router headers to the cache key so the two response types are stored separately. On CloudFront, the managed CachingOptimized policy is the usual culprit — it is the sensible default for static sites and its HeaderBehavior is none, meaning no request header influences the cache key at all.

{
  "Name": "nextjs-rsc",
  "DefaultTTL": 86400,
  "MaxTTL": 31536000,
  "MinTTL": 0,
  "ParametersInCacheKeyAndForwardedToOrigin": {
    "EnableAcceptEncodingGzip": true,
    "EnableAcceptEncodingBrotli": true,
    "HeadersConfig": {
      "HeaderBehavior": "whitelist",
      "Headers": { "Quantity": 2, "Items": ["RSC", "Next-Router-Prefetch"] }
    },
    "CookiesConfig": { "CookieBehavior": "none" },
    "QueryStringsConfig": { "QueryStringBehavior": "none" }
  }
}
A CloudFront cache policy that separates HTML from component payloads. Attach it to the default behaviour; leave /_next/static/* on CachingOptimized, since hashed assets are never RSC.

Both headers matter. RSC separates component payloads from HTML; Next-Router-Prefetch separates a prefetch from a full navigation payload, which are also not the same response. Two headers means at most four variants per URL — a negligible cost against serving Googlebot an empty page.

The principle is identical on any CDN. On Cloudflare it is a Cache Rule with a custom cache key including those headers; on Fastly it is a vary on them. What matters is that no CDN in front of an App Router application may treat the RSC header as irrelevant.

The other half of this problem shows up as URLs like /services?_rsc=a1b2c3 appearing in Search Console. Next.js appends that parameter to prefetch requests, and those URLs can end up discovered and crawled.

They are far less dangerous than the cache issue, and the instinct to block them in robots.txt is worth resisting. A blocked URL cannot be crawled, so Google never sees the canonical tag that would consolidate it — you trade a tidy report for an unresolved duplicate. Two things handle it properly:

  • A self-referencing canonical on every page, pointing at the clean URL without the parameter. Google consolidates the variant into the canonical on its own.
  • A cache key that ignores query strings, so ?_rsc= requests without the header are served the same HTML as the clean URL rather than a payload.

What we would take from this

The uncomfortable part of this bug is not the fix, which is four lines of configuration. It is that our own site was serving component payloads to crawlers while every dashboard said it was healthy — indexed, ranking, all audits passing. We only found it because a crawler with a real browser engine disagreed with a curl request, and we chose to investigate rather than assume the tool was broken.

  • Crawl your own site with something that executes JavaScript, not only with a fetcher
  • When a tool reports something you know is false, find out why before dismissing it
  • Treat any CDN in front of a rendering framework as part of the SEO surface, not as infrastructure someone else owns

If you run Next.js behind a CDN, the two-minute test above is worth doing today. It costs nothing, and the failure it detects is one that no amount of good content or clean markup will compensate for.

Frequently asked questions

What does text/x-component mean in a Next.js response?
It is the content type of a React Server Component payload — the serialized page description the App Router client uses for navigation. It is correct for a prefetch request carrying the RSC header, and a bug when returned to a plain navigation or a crawler.
Can this actually get pages deindexed?
It can. A crawler receiving a component payload sees no title, no headings and no readable body content. Sustained across recrawls that reads as an empty or broken page, which typically shows up as pages moving into "Crawled — currently not indexed".
Does this affect Vercel-hosted Next.js sites?
Vercel configures its own edge network for this, so it is not the common case there. The risk belongs to self-managed setups — CloudFront, Cloudflare, Fastly or an nginx cache in front of a Next.js server — where the cache key is configured by you and header-blind defaults are the norm.
Why did curl show the page was fine?
Because curl does not execute JavaScript and therefore never issues the prefetch that poisons the cache entry. The bad entry is created by real browsers and expires on its own, so a fetch-only check can pass repeatedly while the problem is live.
Should I block _rsc URLs in robots.txt?
Generally no. Blocking them stops Google crawling those URLs, which means it never sees the canonical tag that would consolidate them into the clean URL. A self-referencing canonical on every page handles it properly and leaves the signals intact.

Keep reading

Related guides and the services that put this into practice.

Service

Technical SEO

Fix the crawling, indexing, speed and rendering problems that quietly cap everything else you do in SEO.

View service

Service

SEO Audit

A full diagnostic of your website — technical, on-page, content, local and competitive — delivered as a prioritized action list.

View service

Article

A technical SEO checklist that finds real problems

Ordered by impact, not by how easy it is to tick off. Start at the top — most sites have a problem in the first three items.

Read article

Reading is useful. Applying it to your site is better.

The free audit checks your indexation, search visibility and competitors — then tells you what to fix first.

Prefer to talk? +91 87674 31502