<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: carlosar</title>
    <description>The latest articles on DEV Community by carlosar (@carlosar).</description>
    <link>https://dev.to/carlosar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4000244%2Fa48197d1-eb20-4f63-8ea2-925532efe51b.png</url>
      <title>DEV Community: carlosar</title>
      <link>https://dev.to/carlosar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlosar"/>
    <language>en</language>
    <item>
      <title>I built a VS Code linter that catches Firebase mistakes before they cost you money</title>
      <dc:creator>carlosar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 07:55:58 +0000</pubDate>
      <link>https://dev.to/carlosar/i-built-a-vs-code-linter-that-catches-firebase-mistakes-before-they-cost-you-money-kpc</link>
      <guid>https://dev.to/carlosar/i-built-a-vs-code-linter-that-catches-firebase-mistakes-before-they-cost-you-money-kpc</guid>
      <description>&lt;p&gt;In May 2026, our dev Firestore environment quietly racked up a $95 bill we weren't expecting — 98 million &lt;code&gt;dunning_log&lt;/code&gt; reads, 37 million &lt;code&gt;invoice&lt;/code&gt; reads, and 1.6 million &lt;code&gt;settings&lt;/code&gt; reads, all in one billing period. For a dev environment. Nobody was running a load test.&lt;/p&gt;

&lt;p&gt;The root cause was one &lt;code&gt;useEffect&lt;/code&gt; in &lt;code&gt;App.tsx&lt;/code&gt; with two unrelated mistakes stacked on top of each other: a dependency on &lt;code&gt;activeTab&lt;/code&gt; (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).&lt;/p&gt;

&lt;p&gt;It looked completely normal in code review. No linter caught it. No test failed. It shipped, it ran, and it billed.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CostGuard does
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuq3u8onrr3dfx9u7vatb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuq3u8onrr3dfx9u7vatb.png" alt="CostGuard inline squiggle on a violation" width="798" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It catches 17 specific patterns across three risk categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — unbounded Firestore reads, reads in loops, real-time listeners triggered by UI state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — fetch/axios in loops, writes without batching, httpsCallable in loops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Leaks&lt;/strong&gt; — missing onSnapshot cleanup, setInterval without cleanup, event listeners never removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each violation gets a risk score. Files above 25 points are HIGH risk. Files above 0 are MEDIUM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8znmvw32rgn0jypnc3cg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8znmvw32rgn0jypnc3cg.png" alt="CodeLens risk score at the top of a file" width="631" height="187"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern that cost us $95
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;dunning_log&lt;/code&gt; collection alone took 98 million reads. Here's a simplified version of the effect that did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DunningWidget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;activeTab&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;activeTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFirebaseConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// new object reference on every render — never memoized&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dunning_log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;adminGetStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;activeTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one effect has two independent problems stacked on top of each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;activeTab&lt;/code&gt; 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 &lt;strong&gt;FCG003&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getFirebaseConfig()&lt;/code&gt; 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 &lt;code&gt;onSnapshot&lt;/code&gt; call in the body, CostGuard flags this as &lt;strong&gt;FCG010&lt;/strong&gt;, its compound-risk rule for exactly this shape: an unstable dependency plus an expensive operation in the same effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two separate rules, two separate root causes, one effect, one $95 bill. The same broken pattern repeated for &lt;code&gt;invoice&lt;/code&gt; and &lt;code&gt;settings&lt;/code&gt; accounted for the rest of it.&lt;/p&gt;

&lt;p&gt;The fix addresses both at once — memoize the config, and key the listener on something that actually identifies &lt;em&gt;what&lt;/em&gt; to load, not on UI state or a fresh reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DunningWidget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;workspaceId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getFirebaseConfig&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt; &lt;span class="c1"&gt;// stable reference&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dunning_log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;adminGetStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;// workspaceId only changes when the data scope actually changes — not on every tab click&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CostGuard catches both problems and underlines them in red the moment you type them. No save required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three layers of protection
&lt;/h2&gt;

&lt;p&gt;The extension is the first layer — it catches issues as you write. But CostGuard also installs two more layers through a setup wizard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-commit hook&lt;/strong&gt; — blocks &lt;code&gt;git commit&lt;/code&gt; if the staged files have HIGH risk violations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ 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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub Actions PR gate&lt;/strong&gt; — posts a risk card on every PR and blocks merges above your threshold:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fc9zk11zfxq1b3b5esr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fc9zk11zfxq1b3b5esr.png" alt="CostGuard PR gate risk card" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How to install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VS Code extension&lt;/strong&gt; (zero config, works immediately):&lt;br&gt;
Search "CostGuard" in the VS Code Extensions panel, or install from the Marketplace:&lt;br&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=soarone.costguard" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=soarone.costguard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI for CI/CD pipelines:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; costguard
npx costguard src/ &lt;span class="nt"&gt;--max-risk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HIGH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup wizard inside VS Code will offer to install the pre-commit hook and GitHub Actions workflow automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else it catches
&lt;/h2&gt;

&lt;p&gt;Beyond reads in loops, CostGuard flags:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCG001&lt;/td&gt;
&lt;td&gt;Unstable useEffect deps causing infinite re-renders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG002&lt;/td&gt;
&lt;td&gt;Firestore reads with no .limit() — unbounded collections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG003&lt;/td&gt;
&lt;td&gt;Real-time onSnapshot triggered by UI state changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG004&lt;/td&gt;
&lt;td&gt;onSnapshot with no cleanup (memory leak)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG006&lt;/td&gt;
&lt;td&gt;setInterval with no clearInterval (memory leak)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG010&lt;/td&gt;
&lt;td&gt;Compound pattern: unstable deps + expensive op in same effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCG014&lt;/td&gt;
&lt;td&gt;Fetching entire collection then filtering client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full list of all 17 rules is in the README on GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's open source
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/carlosar/costguard" rel="noopener noreferrer"&gt;https://github.com/carlosar/costguard&lt;/a&gt;&lt;br&gt;
VS Code Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=soarone.costguard" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=soarone.costguard&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/costguard" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/costguard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr406cf9saql4ofqz3m55.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr406cf9saql4ofqz3m55.gif" alt="CostGuard live demo" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you ship Firebase + React, install CostGuard now and let it scan your code before your next bill does:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=soarone.costguard" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=soarone.costguard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's the most expensive Firebase mistake you've made? Drop it in the comments — it might become FCG018.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>react</category>
      <category>vscode</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
