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-Controlgetmax-age=3600(exactly 1 hour) by default. That hour is why HTML looks stale after a deploy. - Set HTML to
no-cache, max-age=0and content-hashed assets (Astro’s/_astro/**, Vite output) tomax-age=31536000, immutableinfirebase.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.
| Layer | Cleared on deploy? | What clears it |
|---|---|---|
| Firebase CDN edge | Yes, automatically every deploy | firebase deploy --only hosting |
| Browser HTTP cache | No | The Cache-Control header you set (no-cache forces revalidation) |
| Service worker cache | No | A 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.appserves new — edge cache differs by host. - An old service worker keeps re-fetching cached assets.
curl -Iagainst the HTML URL shows nocache-controlheader (default fallback applies).
Before you start
- Confirm the build output directory matches
firebase.json(distfor Astro,outfor Next static export). - Have
curlavailable 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
- 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')
- 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" }
]
}
]
}
}
-
HTML uses
no-cache, max-age=0, notno-store.no-cachemeans “store but revalidate every time” — the browser still benefits from local copy but always checks freshness.no-storeforces a full re-download, which is wasteful. -
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. Plainapp.jsdoes not. -
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
-
For a stuck browser, hard-reload (Cmd+Shift+R / Ctrl+Shift+R) once to flush local cache. That clears the local copy; the
no-cacheheader keeps it correct from then on. -
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();
});
- If a bad version went live, roll back to a known-good one. There is no
firebase hosting:rollbackCLI command (as of June 2026) — rollback is a console button, or you clone a specific version by ID. List versions, find theVERSION_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-Controlheaders infirebase.json. curl -sIverifies 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 -sIfrom 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=3600and 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
immutableon HTML — browsers will never revalidate, even after a hard reload. - Headers set in your framework’s adapter conflicting with
firebase.json— thefirebase.jsonheaderswin for matched paths; confirm the actual response withcurl -sI. - A buggy service worker serving the old bundle indefinitely after deploy (no SW version bump).
- Using
no-storeon HTML whenno-cacheis intended —no-storealso 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-cacheon 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
headersarray takes multiplesourceglob 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=31536000header is now lying. Either hash it via your build tool (Astro and Vite do this by default), or drop that file to a shortmax-ageuntil you do. - Will
no-cachehurt 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 tiny304 Not Modifiedinstead of the full page. That small round-trip is the price of instant updates. - Is there a
firebase hosting:rollbackcommand?: 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.