No infra changes. No refactor. Just smarter caching.
The problem
I recently revisited my Firebase Hosting setup and found that without proper caching, the browser keeps downloading the same JavaScript, CSS, JSON, and image files again and again. That drives up bandwidth usage, slows down repeat visits, and burns through Firebase's free tier much faster than expected.
- Browsers repeatedly download the same JS, CSS, JSON, and image assets.
- Bandwidth usage increases significantly.
- Repeat visits become slower than they need to be.
- Firebase's free hosting tier gets exhausted quickly.
I was trying to stay inside Firebase's 10 GB free tier while
handling about 100k monthly users. Without caching, the math is
rough: 100,000 requests × 575 KB ≈ 57.5 GB. That is
about 47 GB over the free limit.
The fix
The change is simple but high impact:
index.htmlstays uncached withCache-Control: no-cache.- Static assets get
public, max-age=31536000, immutable.
What happens with this setup?
- The browser always checks
index.html, which is a tiny file. - Static assets are cached aggressively.
- Repeat visits reuse cached files instead of re-downloading them.
- New deployments still work correctly because the HTML points to the latest asset filenames.
Real impact
With proper caching in place, repeat visitors download 0 KB, returning visitors usually pay only the cost of a tiny HTML check, and only new visitors pull the full app. That drops total bandwidth from about 57.5 GB to roughly 5.8 GB.
Why this works
Modern build tools like React and Vite generate hashed asset filenames such as
main.a1b2c3.js and main.z9y8x7.js. On every deploy,
the browser fetches a fresh index.html, sees the new filenames, and
downloads only the new files it actually needs.
That means your old files can be cached for a year without becoming dangerous. The cache is tied to the filename, not the conceptual asset. If the filename changes, the browser automatically requests the new one.
Configuration difference
{
"hosting": [
{
"target": "staging",
"public": "public",
"ignore": [
"index.html",
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "service_id",
"region": "US-EAST1"
}
}
]
},
{
"target": "prod",
"public": "public",
"ignore": [
"index.html",
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "service_id",
"region": "US-EAST1"
}
}
]
}
]
}
index.html fresh, cache
immutable assets hard, and let hashed filenames handle safe updates.{
"hosting": [
{
"target": "staging",
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "index.html",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
},
{
"source": "static/**",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
},
{
"source": "**/*.@(js|css|json|webp|png|jpg|svg)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
},
{
"target": "prod",
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "index.html",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
},
{
"source": "static/**",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
},
{
"source": "**/*.@(js|css|json|webp|png|jpg|svg)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}
]
}
Why a one-year cache is safe
The browser may cache main.a1b2c3.js for a year, but when a new
deploy ships and index.html points to
main.z9y8x7.js, the browser requests the new file automatically.
No stale code. No broken updates.
Bonus: atomic deploys
Firebase Hosting uses atomic deploys, so the new index.html and the
new asset files go live together. That avoids broken references between the HTML
shell and the built assets.
Key takeaway
Caching is not just a performance optimization. It directly affects
cost, speed, and deployment
safety. A few lines in firebase.json can save real
money and improve UX at scale. If you are using Firebase Hosting without proper
caching, you are probably overpaying.