DEV Community

Cover image for I built a VS Code linter that catches Firebase mistakes before they cost you money
carlosar
carlosar

Posted on

I built a VS Code linter that catches Firebase mistakes before they cost you money

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.

CostGuard inline squiggle on a violation

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.

CodeLens risk score at the top of a file


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]);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

This one effect has two independent problems stacked on top of each other:

  • activeTab is 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 the onSnapshot call 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
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

GitHub Actions PR gate — posts a risk card on every PR and blocks merges above your threshold:

CostGuard PR gate risk card


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
Enter fullscreen mode Exit fullscreen mode

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

CostGuard live demo

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)