Firebase Hosting cache: force fresh content after deploy

Deployed but visitors still see the old page? Fix it with this firebase.json cache-header config, a curl diagnostic, and the correct rollback command (verified June 2026).

You deployed. The new version is live on *.web.app. But your friend opens the custom domain and still sees yesterday’s headline. This is almost always a cache problem — and counterintuitively, it is usually not Firebase’s CDN. A firebase deploy --only hosting already purges the entire CDN edge cache. The page that lingers is the copy sitting in the visitor’s browser or in a stale service worker. The fix is the right Cache-Control headers in firebase.json so the browser revalidates instead of trusting an old local copy.

TL;DR

  • Firebase Hosting clears its CDN cache on every deploy automatically. Per the official docs, “Firebase Hosting automatically clears all your cached content across the CDN until the next request.”
  • Files you serve with no explicit Cache-Control get max-age=3600 (exactly 1 hour) by default. That hour is why HTML looks stale after a deploy.
  • Set HTML to no-cache, max-age=0 and content-hashed assets (Astro’s /_astro/**, Vite output) to max-age=31536000, immutable in firebase.json.
  • After deploy, verify with curl -sI — not DevTools, which has its own cache layer. A browser hard-reload clears the one stuck local copy.

What actually caches your page

Three layers can hold an old page. Knowing which one is the difference between a 10-second fix and an hour of confusion.

LayerCleared on deploy?What clears it
Firebase CDN edgeYes, automatically every deployfirebase deploy --only hosting
Browser HTTP cacheNoThe Cache-Control header you set (no-cache forces revalidation)
Service worker cacheNoA version bump + skipWaiting() in your SW

The default for any file without an explicit Cache-Control is public, max-age=3600 — one hour (confirmed against Firebase docs, June 2026). That is fine for images and hashed JS/CSS, but on HTML it means visitors keep yesterday’s page for up to an hour after a successful deploy. Firebase also caches 404 responses at the edge for 10 minutes by default, so a route you just added can 404 briefly after going live.

How to tell

  • Deploy succeeded. New content visible only in incognito.
  • Mobile sees new content, desktop sees old (or vice versa) — different cache states.
  • Custom domain serves old content, *.web.app serves new — edge cache differs by host.
  • An old service worker keeps re-fetching cached assets.
  • curl -I against the HTML URL shows no cache-control header (default fallback applies).

Before you start

  • Confirm the build output directory matches firebase.json (dist for Astro, out for Next static export).
  • Have curl available to verify headers — not just devtools, which adds its own behavior.
  • If you ship a service worker, locate its source file before deploying any cache changes.

Step by step

  1. Diagnose first. Confirm the cache state on the slow path:
curl -sI https://yourdomain.com/ | grep -iE 'cache-control|age|x-cache'
# typical bad state:
#   age: 1840
#   x-cache: HIT
#   (no cache-control or 'public, max-age=3600')
  1. Edit firebase.json — full working config:
{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "cleanUrls": true,
    "trailingSlash": true,
    "headers": [
      {
        "source": "**/*.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache, max-age=0" }
        ]
      },
      {
        "source": "/_astro/**",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "**/*.@(js|css|woff2)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "**/*.@(jpg|jpeg|png|webp|avif|svg)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=2592000" }
        ]
      },
      {
        "source": "/sitemap*.xml",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=3600" }
        ]
      }
    ]
  }
}
  1. HTML uses no-cache, max-age=0, not no-store. no-cache means “store but revalidate every time” — the browser still benefits from local copy but always checks freshness. no-store forces a full re-download, which is wasteful.

  2. Hashed static assets get immutable. Only safe when the filename changes on every content change. Astro’s /_astro/** and Vite’s hashed outputs both qualify. Plain app.js does not.

  3. Deploy and verify the headers at the edge:

npm run build
firebase deploy --only hosting

# 30 seconds later:
curl -sI https://yourdomain.com/                          | grep -i cache-control
# cache-control: no-cache, max-age=0

curl -sI https://yourdomain.com/_astro/index.abc123.css   | grep -i cache-control
# cache-control: public, max-age=31536000, immutable
  1. For a stuck browser, hard-reload (Cmd+Shift+R / Ctrl+Shift+R) once to flush local cache. That clears the local copy; the no-cache header keeps it correct from then on.

  2. If you ship a service worker, ship a new SW version on every deploy and call skipWaiting() to avoid week-long stale caches:

// public/sw.js
const VERSION = 'v2026-05-22-1';                 // bump on every deploy

self.addEventListener('install', (e) => {
  self.skipWaiting();                            // activate immediately
});

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => !k.includes(VERSION)).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});
  1. If a bad version went live, roll back to a known-good one. There is no firebase hosting:rollback CLI command (as of June 2026) — rollback is a console button, or you clone a specific version by ID. List versions, find the VERSION_ID, then clone it onto the live channel:
firebase hosting:versions:list
# clone a specific prior version (note the @VERSION_ID syntax) onto live:
firebase hosting:clone YOURPROJECT:@VERSION_ID YOURPROJECT:live

The cleaner path for most people: Firebase console → Hosting → Release history → hover the good release → menu → Roll back. Either way the rollback is itself a deploy, so it clears the CDN edge again automatically.

Implementation checklist

  • HTML, hashed assets, images, and sitemap have explicit Cache-Control headers in firebase.json.
  • curl -sI verifies headers after every deploy.
  • Service worker (if any) bumps version + calls skipWaiting() per deploy.
  • Build outputs hashed filenames for /_astro/** or equivalent.

After-launch verification

  • curl -sI from a different network or via a free remote tool (e.g. WebPageTest) — confirms edge cache is updated, not just your local node.
  • Open the site in incognito and a logged-out browser; both should see the new version immediately.
  • Service worker registration in DevTools → Application → Service Workers shows the new version active.

Common pitfalls

  • Leaving HTML on the default max-age=3600 and chasing “ghost” stale pages for up to an hour after every deploy.
  • Blaming the Firebase CDN. Deploy already purged it. If the page is still old, the stale copy is in the browser or a service worker.
  • Setting immutable on HTML — browsers will never revalidate, even after a hard reload.
  • Headers set in your framework’s adapter conflicting with firebase.json — the firebase.json headers win for matched paths; confirm the actual response with curl -sI.
  • A buggy service worker serving the old bundle indefinitely after deploy (no SW version bump).
  • Using no-store on HTML when no-cache is intended — no-store also disables the browser back/forward cache, which hurts navigation speed for no benefit here.

FAQ

  • How do I force a refresh for everyone right now?: Redeploy. That purges the entire CDN edge cache automatically. Visitors whose browser still holds a long-cached copy revalidate on their next request, which is exactly what no-cache on HTML guarantees.
  • Does Firebase Hosting purge the CDN cache on deploy?: Yes — and for all files, not just changed ones. The docs state Firebase “automatically clears all your cached content across the CDN until the next request.” So persistent staleness is a browser or service-worker problem, not a CDN one.
  • Can I set different cache rules for different paths?: Yes. The headers array takes multiple source glob patterns, each with their own headers. When two patterns match the same file, the first matching block in the array wins for that header.
  • My page is fresh but my CSS is still old — why?: Your CSS filename is probably not content-hashed, so its immutable, max-age=31536000 header is now lying. Either hash it via your build tool (Astro and Vite do this by default), or drop that file to a short max-age until you do.
  • Will no-cache hurt performance?: Barely, for HTML. The browser keeps its local copy and revalidates with a conditional GET (If-None-Match); when nothing changed, Firebase returns a tiny 304 Not Modified instead of the full page. That small round-trip is the price of instant updates.
  • Is there a firebase hosting:rollback command?: No (June 2026). Roll back from the console (Hosting → Release history → Roll back) or clone a specific version: firebase hosting:clone PROJECT:@VERSION_ID PROJECT:live.

Tags: #Indie dev #Firebase #Hosting #Troubleshooting