How I Run 114 Posts, 98 Tools and 32 Games on Firebase Blaze for $0 a Month
DEV Community

How I Run 114 Posts, 98 Tools and 32 Games on Firebase Blaze for $0 a Month

Firebase's Blaze plan has a reputation problem. People hear "pay as you go", picture the horror stories - the recursive Cloud Function that wrote back into the collection that triggered it, the forgotten listener on a 400,000-document collection - and stay on Spark until Spark isn't enough. Then they upgrade and quietly brace for the email. My portfolio site has been on Blaze for months. It serves 114 blog posts, 98 interactive browser tools and 32 browser games, with a Firestore-backed CMS behind an admin panel, images in Cloud Storage, and the whole thing on Firebase Hosting behind a custom domain. Last month's bill was $0.00. So was the month before, and the one before that. Not "basically free" - actually zero. That isn't luck, and it isn't because the site is small. It's about six decisions, each of which said no to something Firebase was very happy to sell me. This post is all of them: what Blaze actually changes, why I don't deploy a single Cloud Function, where large binaries live instead of Storage, how 130 interactive pages generate zero writes, the Firestore read-amplification traps that quietly turn a content site into a metered one, and where this approach stops working. What Blaze actually changes, and what it doesn't The first misconception worth killing: upgrading to Blaze does not switch the free tier off. The no-cost quotas carry over unchanged. What Blaze removes is the hard ceiling that made Spark refuse work once you hit a limit; in its place you get a meter that starts at zero and bills only the usage above those same quotas. Roughly what you get for nothing, per project, on either plan: - Firestore - 1 GiB stored, 50,000 document reads/day, 20,000 writes, 20,000 deletes, 10 GiB/month egress. - Cloud Storage - 5 GB stored, 1 GB/day downloaded, 20,000 upload and 50,000 download operations per day. - Hosting - 10 GB stored, 360 MB/day transferred, custom domain and TLS included. - Authentication - free for the common providers. SMS-based phone auth is not. Check the current numbers before planning around them - Google moves them - but the shape is stable, and the shape is what matters. Note which resources meter per day (Firestore reads, Storage downloads, Hosting transfer) and which per month: daily quotas reset, so one bad day costs one bad day, not a month. So the goal was never "avoid Firebase". It's stay under the line, and make crossing it structurally impossible rather than merely unlikely. Those are different engineering problems. The first is easy on a quiet Tuesday; the second is what matters when a tool link lands on Hacker News at 3am while you're asleep. Rule one: no Cloud Functions, ever This is the rule I'm most rigid about, and it surprises people, because Functions are the most useful thing on the platform. The problem isn't the invocation price - two million a month is generous and I'd never come close. The problem is that Cloud Functions is the one Firebase product where a bug can bill you in a loop. A Firestore-triggered function that writes back into the collection it watches will re-trigger itself, as fast as the runtime can spawn instances. Every iteration is an invocation, plus a write, plus a read, plus CPU-seconds. You find out when the alert arrives, and by then the graph is already vertical. Second, deploying Functions isn't free even when they never run. Each deploy builds a container image; those land in Artifact Registry, the build runs on Cloud Build, and both have modest no-cost allowances a few dozen deploys will chew through. Cents, not dollars - but "$0.00" and "$0.31" are different lines on an invoice. Third, and most important: almost everything people reach for Functions to do, a content site can do somewhere else for free. - Resize an image on upload? Resize it before you upload it. My admin panel does it in the browser, so the file reaching Storage is already the right size. - Sitemap, RSS feed, Open Graph share cards? Generate them at build time in CI and deploy them as static files. Mine come from a script in GitHub Actions. - Email on form submit? A third-party form endpoint, or a workflow triggered by repository_dispatch , does it without a Firebase bill. - Aggregation - post counts, tag counts, related posts? Compute it in the same CI job that publishes the content and write one summary document. The mental shift is that a build step is a free Cloud Function running on somebody else's meter. CI minutes on a public repository cost nothing, a build can't run away with itself the way a trigger can, and if it breaks it just breaks - it doesn't spend. The honest cost is that I have no trusted server-side execution: no private API keys, no SSR, no webhook receivers. Those are real capabilities and I gave them up deliberately. If I needed one I'd deploy a function with maxInstances clamped low - and stop describing the site as free. Rule two: large binaries live on GitHub Releases, not Storage This one is pure arithmetic, and it's the rule that would otherwise have broken the bank first. My Flutter tools family ships real installers - APKs, macOS DMGs, Windows builds. Call an APK 45 MB, modest for a Flutter app with bundled assets. Cloud Storage gives you 1 GB/day of downloads at no cost. That's twenty-two downloads a day. Twenty-three people on a good day and the meter starts running. Worse, download bandwidth is exactly the metric you want to grow. Every good thing that happens - a post that ranks, a tool that gets shared - pushes it up. A cost structure where success is the failure mode is a bad idea even while the numbers are small. So no binary ever touches Cloud Storage. They live as GitHub Release assets and the site links to them. GitHub doesn't meter bandwidth on release assets for public repositories, the per-file limit is comfortably above anything I ship, and every release gets a permanent URL a download button can point at. Firestore stores the URL, version string and file size - a few hundred bytes per app instead of tens of megabytes plus egress. The bonus I didn't plan for: release assets give you versioned history free, so I never had to build a "previous versions" UI over Storage listings. And Storage only holds what the rules allow What's left in Storage is deliberately boring: cover images, a few screenshots, one PDF (my CV). Nothing else gets in, and that's enforced in the rules file rather than the upload UI - because the UI is the part an attacker skips. rules_version = '2'; service firebase.storage { match /b/{bucket}/o { function isAdmin() { return request.auth != null && request.auth.token.admin == true; } function underCap() { return request.resource.size < 10 * 1024 * 1024; } match /images/{allPaths=} { allow read: if true; allow write: if isAdmin() && underCap() && request.resource.contentType.matches('image/.*'); } match /docs/{file} { allow read: if true; allow write: if isAdmin() && underCap() && request.resource.contentType == 'application/pdf'; } match /{allPaths=} { allow read, write: if false; } } } Three things do real work. underCap() limits a single upload to 10 MB, so nobody - including me on a careless evening - can park a 400 MB screen recording in the bucket. The contentType match stops the bucket quietly becoming a general file host. And the final catch-all denies everything not explicitly matched above, the rule I'd keep if I could only keep one: default-deny is the only storage rule that survives you forgetting about a path. Be precise about contentType , though: it comes from the client and can be spoofed, so it's a cost guard rather than a security guarantee. With the size cap and admin-only writes it does the job I need - making "accidentally expensive" impossible. Rule three: everything interactive runs entirely on the client 130 interactive pages - 98 tools and 32 games - and not one of them writes to Firestore. Not a score, not a session, not an analytics ping. That was a constraint from day one, and it's the biggest single reason the counters stay flat no matter what traffic does. A JSON formatter, a colour-contrast checker, a unit converter, a game of solitaire: none of these need a server. They need a pure function and some state. The function is Dart compiled to JavaScript, and the state lives in localStorage . // Tools persist their own state locally. No network, no writes, no cost. final prefs = await SharedPreferences.getInstance(); await prefs.setString('json_formatter.indent', '2'); await prefs.setStringList('solitaire.best_times', best.take(10).toList()); On Flutter web shared_preferences is backed by localStorage , so this is a synchronous browser API wearing a Dart shape. High scores, preferences, recent inputs, undo history, theme choice - all per-device, none per-account, and each of those decisions deletes a Firestore write from the design before it exists. Consider the alternative. Say the games get a modest 3,000 plays a day and each writes a score document at the end. That's 3,000 writes, comfortably inside the 20,000/day quota - fine so far. Now add a leaderboard, because leaderboards are obviously good: every player who finishes reads the top twenty to see where they landed. That's 60,000 reads a day from a feature nobody asked for, and you're over the 50,000 line on a modest day. Read amplification is almost always a product decision wearing an engineering costume. The trade is real: no cross-device sync, no global leaderboards, no history on a new laptop. For free browser tools that's the right call, and a better privacy story besides. If I ever add accounts it'll be for one feature with its own read budget, not a blanket "store everything and see". Rule four: the Firestore read traps that generate the actual bill Firestore bills per document read, and the ways to accidentally read a lot of documents are better disguised than people expect. These are the three that caught me. Unbounded snapshot listeners collection('posts').snapshots() on a 114-document collection charges 11

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.