In May 2026, our dev Firestore environment quietly racked up a $95 bill we weren't expecting — 98 million dunning_log reads, 37 million invoice reads, and 1.6 million settings reads, all in one billing period. For a dev environment. Nobody was running a load test.
The root cause was one useEffect in App.tsx with two unrelated mistakes stacked on top of each other: a dependency on activeTab (so every tab switch tore down and re-subscribed every Firestore listener from scratch) and a dependency on a function call that returned a new object on every render (so the same teardown-and-resubscribe happened on every render too, tab switch or not).
It looked completely normal in code review. No linter caught it. No test failed. It shipped, it ran, and it billed.
So I built CostGuard — not just to fix that one bug, but to make sure the exact class of mistake (unbounded reads, listener deps that cause re-subscription storms, missing snapshot cleanup, runaway intervals) gets caught the moment it's typed, on any project, by anyone on the team.
What CostGuard does
CostGuard is a VS Code extension and CLI tool that does static analysis on your TypeScript/JavaScript files and flags the patterns that inflate Firebase bills — as you type, before you commit, and before you deploy.
It catches 17 specific patterns across three risk categories:
- Cost — unbounded Firestore reads, reads in loops, real-time listeners triggered by UI state
- Scalability — fetch/axios in loops, writes without batching, httpsCallable in loops
- Memory Leaks — missing onSnapshot cleanup, setInterval without cleanup, event listeners never removed
Each violation gets a risk score. Files above 25 points are HIGH risk. Files above 0 are MEDIUM.
The pattern that cost us $95
The dunning_log collection alone took 98 million reads. Here's a simplified version of the effect that did it:
function DunningWidget({ activeTab }: { activeTab: string }) {
const config = getFirebaseConfig(); // new object reference on every render — never memoized
const [stats, setStats] = useState(null);
useEffect(() => {
const unsubscribe = onSnapshot(collection(db, 'dunning_log'), snap => {
setStats(adminGetStats(snap, config));
});
return () => unsubscribe();
}, [activeTab, config]);
// ...
}
This one effect has two independent problems stacked on top of each other:
-
activeTabis UI state with nothing to do with which data to load — but because it's in the dependency array, the listener tears down and re-subscribes (re-reading the full collection) on every tab switch. CostGuard flags this as FCG003. -
getFirebaseConfig()returns a brand-new object every render, and it's also in the dependency array — so the effect re-runs (and re-subscribes) on every single render, not just tab switches. Combined with theonSnapshotcall in the body, CostGuard flags this as FCG010, its compound-risk rule for exactly this shape: an unstable dependency plus an expensive operation in the same effect.
Two separate rules, two separate root causes, one effect, one $95 bill. The same broken pattern repeated for invoice and settings accounted for the rest of it.
The fix addresses both at once — memoize the config, and key the listener on something that actually identifies what to load, not on UI state or a fresh reference:
function DunningWidget({ workspaceId }: { workspaceId: string }) {
const config = useMemo(() => getFirebaseConfig(), []); // stable reference
useEffect(() => {
const unsubscribe = onSnapshot(collection(db, 'dunning_log'), snap => {
setStats(adminGetStats(snap, config));
});
return () => unsubscribe();
}, [workspaceId, config]);
// workspaceId only changes when the data scope actually changes — not on every tab click
}
CostGuard catches both problems and underlines them in red the moment you type them. No save required.
Three layers of protection
The extension is the first layer — it catches issues as you write. But CostGuard also installs two more layers through a setup wizard:
Pre-commit hook — blocks git commit if the staged files have HIGH risk violations:
$ git commit -m "feat: add user dashboard"
[CostGuard] HIGH risk detected in src/Dashboard.tsx
FCG005 (line 23): Firestore read inside loop — 20 pts
FCG002 (line 31): Unbounded getDocs — 18 pts
Commit blocked. Fix violations or run with --no-verify to skip.
GitHub Actions PR gate — posts a risk card on every PR and blocks merges above your threshold:
How to install
VS Code extension (zero config, works immediately):
Search "CostGuard" in the VS Code Extensions panel, or install from the Marketplace:
https://marketplace.visualstudio.com/items?itemName=soarone.costguard
CLI for CI/CD pipelines:
npm install -D costguard
npx costguard src/ --max-risk=HIGH
The setup wizard inside VS Code will offer to install the pre-commit hook and GitHub Actions workflow automatically.
What else it catches
Beyond reads in loops, CostGuard flags:
| Rule | What it catches |
|---|---|
| FCG001 | Unstable useEffect deps causing infinite re-renders |
| FCG002 | Firestore reads with no .limit() — unbounded collections |
| FCG003 | Real-time onSnapshot triggered by UI state changes |
| FCG004 | onSnapshot with no cleanup (memory leak) |
| FCG006 | setInterval with no clearInterval (memory leak) |
| FCG010 | Compound pattern: unstable deps + expensive op in same effect |
| FCG014 | Fetching entire collection then filtering client-side |
Full list of all 17 rules is in the README on GitHub.
It's open source
What started as a one-off fix for our own incident has grown past "internal tool" status — it's a published VS Code Marketplace extension with versioned releases, a changelog, CI/CD, pre-commit hooks, and its own test suite. The entire project is MIT licensed. If you work with Firebase and React, I'd love feedback on which patterns you've been burned by that aren't covered yet.
GitHub: https://github.com/carlosar/costguard
VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=soarone.costguard
npm: https://www.npmjs.com/package/costguard
If you ship Firebase + React, install CostGuard now and let it scan your code before your next bill does:
https://marketplace.visualstudio.com/items?itemName=soarone.costguard
What's the most expensive Firebase mistake you've made? Drop it in the comments — it might become FCG018.




Top comments (0)