WriteMySEO / Blog / Why Googlebot Renders Your Page With Stale JavaScript
SEO technology

Why Googlebot Renders Your Page With Stale JavaScript

Google's renderer keeps its own resource cache and ignores your HTTP cache headers. How that produces stale renders, how to detect it, and how to force a refresh.

You shipped a fix to your JavaScript bundle three weeks ago. The browser shows the new behavior. The live test in Search Console looks fine. But the rendered HTML Google has on file still reflects the old code — missing product data, an old canonical injected by the client, a template that never finished hydrating.

The usual explanation offered is "the render queue is slow." That's rarely it. The more common mechanism is that Google's Web Rendering Service keeps its own cache of your JavaScript, CSS, and XHR responses, and that cache does not obey your HTTP cache headers. If the URL of a resource didn't change, Google may still be executing the version it fetched weeks ago.

What the Web Rendering Service actually caches

Google's JavaScript SEO documentation states it plainly: WRS caches resources aggressively and ignores cache headers. That includes Cache-Control, max-age, no-store, and Expires. The stated reason is efficiency — rendering millions of pages means refetching the same bundle for every URL on a site would be enormous waste. From Google's side it's a sensible optimization. From yours it means the renderer can be running code you deleted.

Two consequences follow that people underestimate:

The cache lifetime is not documented. Don't build process around a specific TTL number — nobody outside Google knows it, and it is almost certainly not a fixed constant.

How browser caching and WRS caching differ

BehaviorNormal browserGoogle's WRS
Honors Cache-Control / max-ageYesNo — documented to ignore
Cache lifetimeControlled by your headersInternal, undocumented
Reliably invalidated byExpiry, revalidation, hard reloadA different resource URL
Service workersSupportedNot used; WRS does not register them
localStorage / sessionStorage across page loadsPersistsCleared between page loads
ETag / 304 revalidation on render resourcesYesUnclear — do not rely on it

The service worker and storage rows matter more than they look. If your cache-busting or data-freshness strategy lives in a service worker, it simply doesn't exist from Google's perspective. And if your app reads bootstrap data out of localStorage on second page load, Googlebot always behaves like a first-time visitor.

How a stale resource turns into a wrong render

Stale JavaScript rarely produces a subtly-out-of-date page. It usually produces a broken one, because old code and new backend don't agree:

  1. The old bundle calls an endpoint you removed. The fetch 404s, the promise rejects, an unhandled error stops execution, and the component that writes your main content never mounts. Google indexes the shell.
  2. The old bundle expects an old response shape. The API now returns { items: [...] } instead of { products: [...] }. The render throws on an undefined property. Same outcome.
  3. Code-split chunks mismatch. The entry file is cached, so it requests chunk names that no longer exist on your server. Every lazy-loaded route fails.
  4. Client-injected head tags go stale. If your canonical, title, or structured data are written by JavaScript, the cached bundle writes the old values — and Google uses the rendered DOM for those signals.

This is why a stale-resource problem often looks identical to a "Google can't render our site" problem. The distinguishing symptom is that it's intermittent across URLs and resolves without you changing anything.

Why content fingerprinting is the actual fix

Since the cache key is the URL, the only reliable invalidation is to change the URL whenever the bytes change. That's what content hashing in filenames does, and it's what Google's own documentation recommends.

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash][extname]'
      }
    }
  }
}

Three practical notes:

For API responses consumed during render, the same logic applies: if freshness matters for indexing, the response URL needs to change when the data changes, or the data needs to be in the server-rendered HTML instead of fetched client-side. Server-rendering the content you care about sidesteps this whole class of bug.

How to tell whether this is what's happening to you

There's no report that says "stale resource." You infer it from three sources.

Compare the indexed render to a live test. In URL Inspection, look at the crawled page's rendered HTML, then run a live test. If the live test is correct and the stored version is wrong, you have a staleness problem of some kind. If both are wrong while your browser is right, suspect resource caching or a blocked/erroring fetch rather than the render queue.

Check your logs for asset fetches. Rendering fetches come from Googlebot; live tests come from Google-InspectionTool. If Googlebot requests your HTML constantly but hasn't touched /assets/*.js in weeks, you are being rendered from cache.

grep Googlebot access.log \
  | grep -E '\.(js|css)(\?|$| )' \
  | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head -20

Verify nothing blocks the resources. If /assets/ or your API path is disallowed in robots.txt, WRS won't fetch it at all and will render with whatever it has — or nothing. That failure mode looks like staleness but has a one-line fix.

What to do

  1. Confirm every JS, CSS, and font URL in your build output carries a content hash in the path. If any entry file is stable-named, that's your prime suspect.
  2. Make deploys asset-first, HTML-second, and retain the previous build's hashed assets for at least a crawl cycle.
  3. Audit robots.txt for anything that blocks bundles, APIs, or CDN paths used during render.
  4. Move anything Google must read — canonical, title, meta robots, structured data, primary content — into the server-rendered HTML, so no cached bundle can get it wrong.
  5. Add a defensive error boundary so a failed API call degrades to partial content instead of an empty DOM.
  6. After a significant frontend change, spot-check a handful of URLs by diffing the stored rendered HTML against a live test, and log the result. One screenshot at the time of the incident is worth more than a month of guessing later.
renderingjavascriptcachingGooglebot

WriteMySEO produces marketing content, not legal, medical, financial, or compliance advice. Figures cited reflect publicly reported industry data at time of writing and shift over time.

Get started

We write this well about your industry, every month.

AI-drafted, human-reviewed SEO content on a flat subscription. Blog posts, metadata, schema, and internal links, shipped on a monthly rhythm.

See plans

More from the blog