Your local site works. You run firebase deploy, open the live URL, and get a polite 404. Firebase Hosting 404s are almost always one of five causes, and each takes about ten minutes to fix once you know which one. The trick is to stop guessing and instead reproduce locally, read firebase.json against the build output, and check the response with curl instead of a cache-poisoned browser tab.
TL;DR
- Reproduce first:
npm run build && firebase serve --only hosting, thencurl -sI http://localhost:5000/about/. A 404 here means a build problem; a 200 means a deploy or config problem. - The two most common causes are a wrong
publicdirectory infirebase.jsonand acleanUrls/trailingSlashmismatch with what your framework actually writes to disk. - SSR routes never auto-render on classic Hosting. They need a rewrite to a Cloud Function or Cloud Run service, or you move that app to Firebase App Hosting.
- A stale
**catch-all rewrite to/index.htmlmasks every 404 by serving the homepage HTML. Remove it from static content sites. - Firebase Hosting’s Spark plan is free and allowed for commercial use, so none of these fixes require upgrading.
How Firebase decides what to serve
Firebase Hosting (built on the open-source Superstatic engine) evaluates every request in a fixed priority order. Knowing this order tells you exactly where your 404 is coming from. Per the official configuration docs, the sequence is:
- Reserved
/__/*namespaces (Firebase’s own paths). - Configured
redirects, first matching rule wins. - Exact-match static content in your
publicdirectory. - Configured
rewrites, first matching rule wins. - Your custom
404.html, if present. - The default Firebase 404 page.
So a 404 means the request reached step 6: no static file matched and no rewrite caught it. Every fix below is about getting the request to land on step 3 or 4 instead.
Which cause is it?
| Symptom | Most likely cause |
|---|---|
| Some routes work, others 404 | trailingSlash / cleanUrls mismatch (cause 2) |
All routes 404 except / | Wrong public directory (cause 1) |
| 404 only on routes meant to be SSR’d | Missing rewrite to a Function / Cloud Run (cause 3) |
| 404 only on routes you added today | Build not run, or you deployed an old artifact (cause 4) |
404 on /_astro/... or other hashed assets | Build output is missing the hashed file (cause 5) |
Reproduce locally before you change anything
This single step splits every 404 into “build problem” or “platform problem” and saves you from blind config edits:
npm run build
firebase serve --only hosting
# In another shell:
curl -sI http://localhost:5000/about/ | head -3
# HTTP/1.1 404 ... ← build output problem (file isn't there)
# HTTP/1.1 200 ... ← Firebase config / deploy problem (file is there)
Use curl -sI (or firebase emulators:start if you also run Functions) so browser cache never enters the picture. A failing URL you cannot reproduce in curl is usually a cache artifact, not a real 404.
Cause 1: public points at the wrong directory
firebase init defaults public to public/, but most frameworks build elsewhere: Astro and Vite write to dist/, Next.js static export writes to out/. If public is wrong, step 3 above never matches and every route except a coincidental /index.html 404s.
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"cleanUrls": true,
"trailingSlash": true
}
}
Confirm the route’s file is actually inside that directory:
ls -la dist/about/
# index.html ← good
# (empty) ← framework never generated the route
find dist -name 'about*' -type f
# dist/about.html ← pair with cleanUrls: true
# dist/about/index.html ← pair with trailingSlash: true
Cause 2: cleanUrls / trailingSlash mismatch
This is the subtle one, and it produces the classic “half my pages work” symptom. The two settings are 301 redirect rules, not just cosmetics:
cleanUrls: truedrops the.htmlextension from existing pages and 301-redirects any/about.htmlrequest to/about. As of June 2026 it only applies to pages that exist; a missing page must match aredirectsrule exactly.trailingSlash: true301-redirects to add a trailing slash;trailingSlash: falseredirects to remove one.
Match the setting to what your framework writes to disk. The four valid combinations:
Framework outputs firebase.json Live URL
about.html cleanUrls: true, trailingSlash: false → /about
about.html + /about/ redirect cleanUrls: true, trailingSlash: false → /about
about/index.html cleanUrls: false, trailingSlash: true → /about/
about/index.html cleanUrls: true, trailingSlash: true → /about/ (cleanest)
Astro’s build.format: 'directory' (the default) outputs about/index.html, so pair it with trailingSlash: true. One trap worth flagging: a combination of trailingSlash: false and cleanUrls: false has historically triggered an infinite-redirect loop in Superstatic, so avoid that pair unless you have tested it.
Cause 3: SSR routes have no rewrite
Classic Firebase Hosting serves static files only. It does not run your server code, so any route that should be server-rendered 404s unless you rewrite it to a backend. You have two paths as of June 2026:
Keep classic Hosting and add a rewrite to a Cloud Function or Cloud Run service:
{
"hosting": {
"rewrites": [
{ "source": "/api/**", "function": "api" },
{ "source": "/render/**", "run": { "serviceId": "ssr-render", "region": "us-central1" } }
]
}
}
Gotchas that produce a 404 or Function not found even with the rewrite present:
- The
serviceIdorregiondoes not match the deployed service. Runfirebase functions:list(or check Cloud Run) and confirm both fields match exactly. - The backend was never deployed. Deploy both together:
firebase deploy --only functions,hosting. For Cloud Run, deploy the service before the Hosting config so the rewrite has a live target. - A public Cloud Run service needs
--allow-unauthenticated, or callers get an error instead of your page.
Or move to Firebase App Hosting, which went GA in April 2025 and is now Google’s recommended home for SSR frameworks like Next.js and Angular. Note that the older Hosting “web frameworks” experiment is closed to new Next.js participants, so new SSR projects should start on App Hosting rather than wiring rewrites by hand.
Cause 4: you deployed an old artifact
If only today’s new routes 404, the build probably did not run, or CI deployed a stale dist/. Check that the live release matches your latest build:
firebase hosting:releases:list
# release 2026-05-22 14:02 <hash> ← compare timestamp with your last build
curl -sI https://yourdomain.com/about/ | grep -i x-served-by
# usually shows a release hash you can match against the list
The fix is almost always a CI ordering bug: make sure the workflow runs npm run build before firebase deploy, and that it deploys the build output directory, not the source.
Cause 5: hashed asset 404s
A 404 on /_astro/... or other content-hashed files means the deployed HTML references a hash that is not in the upload, usually a partial deploy or a CDN holding an old index.html that points at filenames the new build renamed. Re-run a clean build and full deploy, then bypass cache while testing:
curl --header 'Cache-Control: no-cache' -sI https://yourdomain.com/_astro/<file>.js | head -3
The catch-all rewrite that hides every 404
Watch for this leftover, common in SPA templates:
{ "source": "**", "destination": "/index.html" }
It silently turns every unmatched path into the homepage HTML. Technically not a 404, but it breaks SEO and deep links because every URL serves the same shell. Astro and most static content frameworks do not need it. Remove it for content sites; keep it only for a genuine client-routed SPA.
Verify the fix
- Every previously-failing URL now returns 200 via
curl -sI. - A known-bad URL still returns 404, not 200, which proves your
404.htmlworks. - Search Console URL Inspection shows “URL is on Google” or at least “Page fetched” for the affected pages.
FAQ
Why does / work but /about 404?
Almost always trailingSlash or cleanUrls. Either the file is about.html and you have not enabled cleanUrls, or the file is about/index.html and trailingSlash is false. Match the setting to what your build writes to disk.
Should I enable cleanUrls?
Yes for most content sites; it produces tidier URLs and 301-redirects the .html form for you. Just keep your internal links consistent with the setting so you do not chain redirects.
My route is supposed to be SSR. Why does it 404?
Classic Firebase Hosting does not auto-render. You need a rewrite from that path to a Cloud Function or Cloud Run service, or you host the app on Firebase App Hosting. If the rewrite is in firebase.json but the response is still 404 or “function not found”, the serviceId/region almost certainly does not match the deployed backend (Firebase function not found).
My rewrite block looks correct but the URL still serves the static file. A real file at the same path shadows the rewrite, because exact-match static content (step 3) is evaluated before rewrites (step 4). The rule may also sit below a more general one. Walk through why rewrites do not fire.
I see the file in dist/ but it still 404s. Why?
The deploy probably did not upload it. Confirm with firebase hosting:releases:list and compare the timestamp to your last build; CI may have skipped the build step or deployed the wrong directory.
Does fixing this require the Blaze plan? No. Firebase Hosting’s Spark plan is free and permitted for commercial use, so static-content 404 fixes cost nothing. You only need Blaze if you add Cloud Functions or Cloud Run for SSR.