Skip to content
| Marketplace
Sign in
Visual Studio Code>Linters>CostGuardNew to Visual Studio Code? Get it now.
CostGuard

CostGuard

soarone

|
6 installs
| (0) | Free
Detects expensive code patterns — runaway Firebase reads, missing real-time listener cleanup, polling abuse, and React render loops — before they hit production.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Stop Firebase bill shock before it hits production — CostGuard catches expensive patterns as you type, blocks risky commits, and gates your deploys.

CostGuard demo

CostGuard

Catch expensive Firebase and React patterns before they hit production — and before they hit your bill.

CostGuard is a VS Code extension that detects runaway Firestore reads, missing listener cleanup, render loops, and other cost-heavy patterns as you write code. It adds inline squiggles, a per-file risk score, and optional gates that block bad code from being committed, merged, or deployed.

Install from the VS Code Marketplace →


What it catches

Example: one unbounded read can quietly cost ~$90/month

// Bad — reads every document in the collection, on every load
const snap = await getDocs(collection(db, 'invoices')); // ← FCG002

If invoices has 10,000 documents and this query runs on a dashboard visited by 500 users a day:

10,000 docs × 500 loads/day = 5,000,000 reads/day
5,000,000 × 30 days         = 150,000,000 reads/month
150,000,000 / 100,000 × $0.06 (Firestore read price) ≈ $90/month

— for one query, before the feature that uses it has shipped. CostGuard flags this the instant you type it, with the fix inline:

// Fix — cap it, then paginate or query for more as needed
const snap = await getDocs(query(collection(db, 'invoices'), limit(50)));

Features

Live diagnostics

Squiggles appear as you type (500ms debounce). No save required.

Copy fix prompt for AI

Click the lightbulb (or Cmd/Ctrl+.) on any squiggle and choose Copy fix prompt for AI. CostGuard copies the violation message plus the surrounding code to your clipboard as a ready-to-paste prompt — drop it straight into ChatGPT, Claude, or Copilot Chat to get a fix.

Risk scoring

Every flagged file gets a score and a breakdown by risk category, visible inline and in the status bar.

$(shield) Risk Score: 82/100   Cost: HIGH   Scalability: MEDIUM   Memory Leak: HIGH

Deployment gates

Three layers that stop risky code before it ships:

Gate When it runs Blocks on
Pre-commit hook git commit HIGH risk
GitHub Actions PR gate Every pull request HIGH risk
Deploy gate firebase deploy / npm run deploy MEDIUM+ risk

Installation

From the Marketplace (recommended): open VS Code → Extensions → search for CostGuard, or run:

code --install-extension soarone.costguard

From a VSIX (offline installs): download the .vsix from the GitHub releases page, then Extensions → ··· menu → Install from VSIX.

After install, the setup prompt appears automatically — choose which protection layers to enable.


Setup Wizard

On first install (and after major version upgrades), a notification appears a few seconds after VS Code loads. Click Set Up Now to open the protection-layer picker:

CostGuard — Choose your protection layers

  ●  Pre-commit Hook         Block commits with HIGH risk violations
  ●  GitHub Actions PR Gate  Post risk card on PRs, block HIGH risk merges
  ○  Deploy Gate             Block firebase deploy / npm run deploy on MEDIUM+ risk

Select what you want, press Enter. CostGuard writes all the necessary files automatically — no manual path setup.

To re-run the wizard at any time: Command Palette → CostGuard: Setup


What it detects

FCG001 — Unstable useEffect dependency error

Objects, arrays, functions, or call results used as useEffect dependencies without useMemo/useCallback. Every render creates a new reference, causing the effect to re-run infinitely — the root cause of most Firestore read runaway bills.

// Bad — query is a new object on every render
const query = { collection: 'invoices', status: 'open' };
useEffect(() => { fetchData(query); }, [query]); // ← FCG001

// Fix
const query = useMemo(() => ({ collection: 'invoices', status: 'open' }), []);

FCG002 — Unbounded Firestore read error

getDocs or collection calls without .limit(). On a large collection this can read thousands of documents in one call.

// Bad
const snap = await getDocs(collection(db, 'invoices')); // ← FCG002

// Fix
const snap = await getDocs(query(collection(db, 'invoices'), limit(50)));

FCG003 — Real-time listener on UI state warning

onSnapshot listeners that re-register whenever UI state like activeTab, selectedId, or showModal changes. Each re-registration opens a new listener and the old one may not be cleaned up.

// Bad — re-registers every time activeTab changes
useEffect(() => {
  return onSnapshot(collection(db, activeTab), ...);
}, [activeTab]); // ← FCG003

FCG004 — onSnapshot without cleanup error

Real-time listeners must return their unsubscribe function. Without it, every component mount adds a new listener that never closes.

// Bad — listener leaks on every remount
useEffect(() => {
  onSnapshot(collection(db, 'invoices'), handler); // ← FCG004
}, []);

// Fix
useEffect(() => {
  return onSnapshot(collection(db, 'invoices'), handler); // return the unsub
}, []);

FCG005 — Firestore read inside a loop error

getDoc or getDocs calls inside for, while, forEach, map, or similar constructs. Each iteration makes a separate network round-trip (N+1 reads).

// Bad — one read per invoice
for (const id of invoiceIds) {
  const doc = await getDoc(doc(db, 'invoices', id)); // ← FCG005
}

// Fix — use getAll / batched reads / a single query

FCG006 — setInterval without cleanup error

setInterval inside useEffect without a corresponding clearInterval return. Intervals stack up on every remount, polling Firebase endlessly.

// Bad
useEffect(() => {
  setInterval(() => fetchStats(), 5000); // ← FCG006
}, []);

// Fix
useEffect(() => {
  const id = setInterval(() => fetchStats(), 5000);
  return () => clearInterval(id);
}, []);

FCG007 — addEventListener without cleanup error

addEventListener inside a useEffect with no removeEventListener returned. Each remount adds a duplicate listener that's never removed, so handlers fire multiple times and the component leaks memory.

// Bad — listener leaks on every remount
useEffect(() => {
  window.addEventListener('resize', handler); // ← FCG007
}, []);

// Fix
useEffect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

FCG008 — fetch/axios inside a loop error

fetch or axios calls inside for, while, forEach, map, or similar constructs. The same N+1 problem as FCG005, but for any billed HTTP API (Stripe, OpenAI, maps, etc.) instead of Firestore.

// Bad — one HTTP request per item
for (const id of ids) {
  await fetch(`/api/items/${id}`); // ← FCG008
}

// Fix — fan out in parallel or use a bulk endpoint
await Promise.all(ids.map(id => fetch(`/api/items/${id}`)));

FCG009 — Firestore read in component body error

getDoc/getDocs (or a Cloud Function call) made directly in a component or hook body instead of inside useEffect. It re-fires on every render with no caching — a component re-rendering 10×/second runs 10 reads/second.

// Bad — refetches every render
function Invoice({ id }: { id: string }) {
  const snap = getDoc(doc(db, 'invoices', id)); // ← FCG009
}

// Fix
useEffect(() => { getDoc(doc(db, 'invoices', id)).then(setInvoice); }, [id]);

FCG010 — Compound render loop error

A useEffect with both an unstable dependency (new object/array/function reference every render) and an expensive operation (getDoc, onSnapshot, fetch) in its body. The two problems amplify each other: the effect re-runs every render, fires the expensive call, which may update state and re-render — an infinite billing loop.

// Bad — config is a new reference every render, and onSnapshot reads on every re-run
const config = getFirebaseConfig();
useEffect(() => {
  return onSnapshot(collection(db, 'dunning_log'), handler);
}, [config]); // ← FCG010

// Fix — stabilize the dependency first
const config = useMemo(() => getFirebaseConfig(), []);

FCG011 — Expensive operation in a high-frequency handler error

getDoc, fetch, or similar calls inside scroll, mousemove, resize, keydown, or input handlers with no debounce/throttle. These events fire up to hundreds of times per second.

// Bad — fires a read on every scroll event
window.addEventListener('scroll', () => getDoc(ref)); // ← FCG011

// Fix
window.addEventListener('scroll', debounce(() => getDoc(ref), 300));

FCG012 — Unbatched Firestore writes in a loop error

addDoc/setDoc/updateDoc/deleteDoc inside a loop fires one separate billed write and network round-trip per iteration.

// Bad — one write per item
for (const item of items) {
  await setDoc(doc(db, 'items', item.id), item); // ← FCG012
}

// Fix — up to 500 ops per batch
const batch = writeBatch(db);
items.forEach(item => batch.set(doc(db, 'items', item.id), item));
await batch.commit();

FCG013 — Polling Firestore with setInterval warning

setInterval/recursive setTimeout wrapping getDoc/getDocs polls on every tick regardless of whether the data changed — every tick is a billed read.

// Bad — billed read every 5 seconds, changed or not
setInterval(() => getDocs(collection(db, 'orders')), 5000); // ← FCG013

// Fix — only fires when data actually changes
onSnapshot(collection(db, 'orders'), handler);

FCG014 — Client-side filtering of an unfiltered read warning

.filter()/.find() applied to a getDocs() result with no .where() clause. The entire collection is fetched and billed before most of it is discarded.

// Bad — fetches everything, keeps a fraction
const snap = await getDocs(collection(db, 'orders'));
const pending = snap.docs.filter(d => d.data().status === 'pending'); // ← FCG014

// Fix — filter server-side
const q = query(collection(db, 'orders'), where('status', '==', 'pending'));
const snap = await getDocs(q);

FCG015 — Read-modify-write instead of FieldValue atomics warning

Reading a document, mutating an array/counter field in JS, then writing the whole document back. Costs an extra read and loses updates under concurrent writes.

// Bad — extra read, race condition under concurrent writes
const data = (await getDoc(ref)).data();
await updateDoc(ref, { count: data.count + 1 }); // ← FCG015

// Fix — atomic, no read needed
await updateDoc(ref, { count: increment(1) });

FCG016 — Cloud Function defined but not exported warning

A Cloud Function assigned to a variable without export. Firebase will never deploy it — it's dead weight in the functions bundle that slows cold starts for every other function in the file.

// Bad — never deployed
const onUserCreate = onDocumentCreated('users/{id}', handler); // ← FCG016

// Fix
export const onUserCreate = onDocumentCreated('users/{id}', handler);

FCG017 — Cloud Function invoked in a loop error

A function created with httpsCallable() invoked inside a loop. Each call bills a separate Cloud Function execution — N items costs N executions instead of one.

// Bad — one billed execution per item
for (const item of items) {
  await sendNotification(item); // ← FCG017
}

// Fix — redesign for a batch payload, or fan out in parallel
await Promise.all(items.map(item => sendNotification(item)));

Risk scoring

Each violation carries a point weight based on its real-world cost impact. Scores are capped at 100.

Rule Risk categories Points
FCG001 Unstable deps Cost + Memory Leak 10
FCG002 Unbounded read Cost + Scalability 18
FCG003 Listener UI dep Cost 12
FCG004 No snapshot cleanup Memory Leak 22
FCG005 Read in loop Cost + Scalability 20
FCG006 No interval cleanup Memory Leak 18
FCG007 No event listener cleanup Memory Leak 15
FCG008 Fetch/axios in loop Cost + Scalability 20
FCG009 Read in render Cost + Scalability 16
FCG010 Compound render loop Cost + Scalability + Memory Leak 35
FCG011 Expensive op in high-freq handler Cost + Scalability 25
FCG012 Unbatched writes in loop Cost + Scalability 20
FCG013 Polling instead of onSnapshot Cost + Scalability 18
FCG014 Client-side filter Cost + Scalability 16
FCG015 Read-modify-write Cost 12
FCG016 Unexported Cloud Function Cost + Scalability 10
FCG017 Cloud Function in loop Cost + Scalability 25

Risk levels per category

Points Level
0 LOW
1 – 24 MEDIUM
25+ HIGH

Deployment gates

Pre-commit hook

Blocks git commit if staged files contain HIGH risk violations.

  CostGuard
  ────────────────────────────────────────────────────────────

  src/invoices.tsx
  Risk 58/100  |  Cost: HIGH  |  Scalability: LOW  |  Memory Leak: HIGH
    ✗  Line 42  [FCG002]  Unbounded Firestore read — add .limit()
    ✗  Line 71  [FCG004]  onSnapshot missing cleanup return

  ────────────────────────────────────────────────────────────
  2 violations in 1 file

  ✗  Blocked — fix HIGH risk violations before proceeding.

Installed automatically by the setup wizard into your git hooks directory — .git/hooks/pre-commit by default, or wherever core.hooksPath points (so husky setups work too).

GitHub Actions PR gate

Runs on every pull request and posts a risk card comment. Fails the required check if HIGH risk violations are found, blocking the merge.

CostGuard PR gate risk card

The workflow is written to .github/workflows/costguard.yml by the setup wizard. Requires costguard in devDependencies (added automatically).

Deploy gate

Blocks deploys when MEDIUM+ risk violations are found — wired in two places by the setup wizard:

  • firebase.json predeploy hooks — added to every deploy target present (hosting, functions, firestore, storage, database), so firebase deploy (and firebase deploy --only <target>) is gated directly.
  • package.json predeploy script — runs automatically before npm run deploy.
firebase deploy      # gated via firebase.json predeploy hooks
npm run deploy       # gated via the npm predeploy script

If your project has no firebase.json, only the npm script gate is installed.


CLI

The analyzer is also available as a command-line tool for use in scripts and CI pipelines.

# Install (or use npx without installing)
npm install -D costguard

# Scan a directory
npx costguard src/

# Scan only staged files (for pre-commit hooks)
npx costguard --staged

# Output JSON for downstream tooling
npx costguard src/ --json

# GitHub Actions annotation format
npx costguard src/ --format=github

# Set the blocking threshold (default: HIGH)
npx costguard src/ --max-risk=MEDIUM

Exit codes: 0 = no violations at or above the --max-risk threshold · 1 = violations at or above the threshold (or invalid arguments)


Configuration

Setting Default Description
costGuard.enable true Enable / disable all diagnostics

Toggle from Settings → search costGuard, or add to your workspace settings.json:

{
  "costGuard.enable": false
}

Commands

Command Description
CostGuard: Setup Re-run the feature setup wizard
CostGuard: Show Risk Score Details Show full risk breakdown for the active file
CostGuard: Generate Report Scan the workspace and write a markdown report (with a copy-paste AI fix prompt) to costguard/
CostGuard: Copy Fix Prompt for AI Copy the selected violation + surrounding code as an AI-ready prompt (also on the lightbulb menu)
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft