<?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: kol kol</title>
    <description>The latest articles on DEV Community by kol kol (@kollittle).</description>
    <link>https://dev.to/kollittle</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%2F3919931%2Fc79f33b2-a2d7-46ef-85a5-74c1b888f1c7.png</url>
      <title>DEV Community: kol kol</title>
      <link>https://dev.to/kollittle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kollittle"/>
    <language>en</language>
    <item>
      <title>I Deleted 40,000 Lines of "Dead Code" — Production Broke in 3 Minutes</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Sat, 04 Jul 2026 14:02:49 +0000</pubDate>
      <link>https://dev.to/kollittle/i-deleted-40000-lines-of-dead-code-production-broke-in-3-minutes-12cc</link>
      <guid>https://dev.to/kollittle/i-deleted-40000-lines-of-dead-code-production-broke-in-3-minutes-12cc</guid>
      <description>&lt;h1&gt;
  
  
  I Deleted 40,000 Lines of "Dead Code" — Production Broke in 3 Minutes
&lt;/h1&gt;

&lt;p&gt;We all hate dead code. It's the junk drawer of your codebase — nobody knows what it does, nobody's touched it in years, and it's just &lt;em&gt;sitting there&lt;/em&gt; taking up space.&lt;/p&gt;

&lt;p&gt;So when I ran our code coverage tool and it showed 40,000 lines with zero references, I felt like a hero. I was about to clean up this mess. The PR description was literally: "Housekeeping — removing unused code."&lt;/p&gt;

&lt;p&gt;I was so wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Our codebase had grown organically over 5 years. Multiple teams, multiple rewrites, at least two "we'll clean it up later" phases that never happened.&lt;/p&gt;

&lt;p&gt;The coverage report was clear: these functions, classes, and modules had zero callers. Zero imports. Zero references. The tool even showed me the exact files. I spent maybe 20 minutes verifying — clicked through a few call chains, searched for dynamic references. Nothing.&lt;/p&gt;

&lt;p&gt;I opened the PR. One reviewer approved in 5 minutes. The other didn't even look. We merged on a Thursday at 4 PM. Classic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened Next
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Minute 1-3: The Silence
&lt;/h3&gt;

&lt;p&gt;Deploy went green. All tests passed. No alerts fired. I closed my laptop feeling productive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minute 4: The First Page
&lt;/h3&gt;

&lt;p&gt;Slack notification: "Checkout is failing."&lt;/p&gt;

&lt;p&gt;Not "checkout is slow." Not "checkout is weird." &lt;em&gt;Failing.&lt;/em&gt; As in, customers couldn't buy things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minute 10: The Investigation
&lt;/h3&gt;

&lt;p&gt;I looked at the error logs. The stack trace pointed to a file I had just deleted. But that's impossible — the coverage tool said nothing called it.&lt;/p&gt;

&lt;p&gt;Then I found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Dynamic References
&lt;/h2&gt;

&lt;p&gt;One of our "legacy" payment integrations used &lt;code&gt;eval()&lt;/code&gt; — yes, &lt;em&gt;eval&lt;/em&gt; — to dynamically construct payment processor class names from a configuration database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config table had: "processor_class": "StripeLegacyProcessor"
&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processor_class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;(api_key)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The coverage tool couldn't see it because the reference was a &lt;em&gt;string in the database&lt;/em&gt;, not code. The static analysis had no way to know that &lt;code&gt;"StripeLegacyProcessor"&lt;/code&gt; in a database row meant &lt;code&gt;from legacy_payments import StripeLegacyProcessor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But wait — it gets worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Web
&lt;/h2&gt;

&lt;p&gt;That one &lt;code&gt;eval()&lt;/code&gt; was just the tip of the iceberg. Once I started searching for &lt;em&gt;all&lt;/em&gt; dynamic references, I found:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database-driven feature routing&lt;/strong&gt; — feature flags stored as class names, resolved at runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; — a YAML file listed "active plugins" by class name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — dynamically loaded report generators based on user permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook handlers&lt;/strong&gt; — URL paths mapped to handler classes via a JSON config&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one was a string reference that my coverage tool couldn't see. Each one broke when I deleted the "dead" code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;I reverted the entire PR in about 2 minutes. But the damage was done — we had maybe 15 minutes of checkout downtime, and I had to explain to the CTO why I'd broken the revenue pipeline for a "housekeeping" PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Coverage Tools Lie (About Dynamically Referenced Code)
&lt;/h3&gt;

&lt;p&gt;Static analysis can only see static references. If your codebase uses &lt;em&gt;any&lt;/em&gt; form of dynamic loading — reflection, eval, metaprogramming, configuration-driven instantiation — your coverage report is incomplete.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Dead Code" Is Often "Code That Calls Itself Indirectly"
&lt;/h3&gt;

&lt;p&gt;Before deleting anything, I should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searched for string literals matching class/function names&lt;/li&gt;
&lt;li&gt;Checked configuration files, database seeds, and migration scripts&lt;/li&gt;
&lt;li&gt;Looked for &lt;code&gt;getattr()&lt;/code&gt;, &lt;code&gt;eval()&lt;/code&gt;, &lt;code&gt;exec()&lt;/code&gt;, &lt;code&gt;importlib.import_module()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Asked the team that originally wrote the code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The Real Dead Code Test Is Runtime, Not Static
&lt;/h3&gt;

&lt;p&gt;A better approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the code&lt;/strong&gt; — add logging to every "suspected dead" function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt; — run in production for at least one full business cycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — only delete functions that logged zero calls over a meaningful period&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flag it first&lt;/strong&gt; — gate the code behind a flag, disable the flag, watch for breakage&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. PR Culture Matters
&lt;/h3&gt;

&lt;p&gt;My reviewers approved a 40,000-line deletion in minutes. That's a process failure. Large deletions should get the same scrutiny as large additions — maybe more, because the risk is invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Aftermath
&lt;/h2&gt;

&lt;p&gt;We eventually cleaned up that codebase — but properly. We added runtime instrumentation, waited two weeks, identified the &lt;em&gt;actually&lt;/em&gt; dead code (about 8,000 lines), and deleted it in small, verified batches.&lt;/p&gt;

&lt;p&gt;The 32,000 lines that &lt;em&gt;looked&lt;/em&gt; dead? They were all referenced dynamically. Every single one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Dead code is like a haunted house — it looks empty, but something's still living in there. Before you start demo lishing walls, make sure nobody's home.&lt;/p&gt;

&lt;p&gt;Next time I see "40,000 lines of unused code," I'm going to assume I just don't understand the codebase well enough yet.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you ever deleted code that wasn't actually dead? Share your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My CI/CD Pipeline Passed for 3 Months — Then I Read the Logs</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Fri, 03 Jul 2026 14:07:21 +0000</pubDate>
      <link>https://dev.to/kollittle/my-cicd-pipeline-passed-for-3-months-then-i-read-the-logs-4mbj</link>
      <guid>https://dev.to/kollittle/my-cicd-pipeline-passed-for-3-months-then-i-read-the-logs-4mbj</guid>
      <description>&lt;h1&gt;
  
  
  My CI/CD Pipeline Passed for 3 Months — Then I Read the Logs
&lt;/h1&gt;

&lt;p&gt;The green checkmark was our favorite color. Every PR. Every merge. Every deploy. All green.&lt;/p&gt;

&lt;p&gt;Then one Tuesday afternoon, a user reported a feature that had been broken for weeks. Not hours. &lt;em&gt;Weeks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I opened the CI pipeline logs. And that's when I realized — our "passing" builds had been lying to us the entire time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Our pipeline looked textbook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lint → 2. Unit tests → 3. Integration tests → 4. Build → 5. Deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every step passed. 100% success rate. For months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;A user filed a bug: "The export button doesn't work." I clicked it myself — nothing happened. No error message, no crash. Just... silence.&lt;/p&gt;

&lt;p&gt;I traced it back to a PR from 11 weeks ago. The pipeline had passed. The code review had been approved. But the feature had been silently broken since day one.&lt;/p&gt;

&lt;p&gt;Here's what went wrong — and it's not what you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Tests That Don't Test
&lt;/h2&gt;

&lt;p&gt;Our integration test suite had a bug. A real bug, in the test code itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What we thought we were testing:&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Export feature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should export user data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// What was actually happening:&lt;/span&gt;
&lt;span class="c1"&gt;// The test mocked the ENTIRE export service.&lt;/span&gt;
&lt;span class="c1"&gt;// So it tested the mock, not the real code.&lt;/span&gt;
&lt;span class="c1"&gt;// The mock always returned { status: 'success' }.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mock was configured at the module level. Every test that imported the export module got the mocked version — including the integration tests that were supposed to catch exactly this kind of bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: Over-Mocking
&lt;/h2&gt;

&lt;p&gt;We had fallen into the classic over-mocking trap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests:&lt;/strong&gt; Mocked everything to isolate the unit → fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests:&lt;/strong&gt; Also mocked everything "for speed" → not fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E tests:&lt;/strong&gt; Didn't cover this specific flow → gap in coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our integration tests were really just expensive unit tests. They verified that our mocks worked correctly. Not that our actual code worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Three changes that made a real difference:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Mock Boundaries
&lt;/h3&gt;

&lt;p&gt;Only mock at unit test level. Integration tests must hit real service boundaries — databases, APIs, file systems. If it's slow, that's a signal, not a problem to hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Contract Tests
&lt;/h3&gt;

&lt;p&gt;Added contract tests between services. If the mock returns something the real service wouldn't, the contract test catches it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Contract test: verify mock matches real behavior&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export mock matches real service contract&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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;mockResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mockExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;realResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockResult&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;realResult&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;mockResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;realResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Pipeline Health Metrics
&lt;/h3&gt;

&lt;p&gt;Started tracking not just pass/fail rates, but &lt;em&gt;what&lt;/em&gt; the tests actually exercised. Coverage numbers went down initially (from 94% to 67% real coverage) — and that was the most honest metric we'd had in months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;A green pipeline doesn't mean your code works. It means your tests passed. Those are two different things.&lt;/p&gt;

&lt;p&gt;The most dangerous bugs aren't the ones that break your pipeline. They're the ones your pipeline says are fine.&lt;/p&gt;

&lt;p&gt;If your CI/CD is all green all the time, ask yourself: are my tests catching bugs, or just confirming that my mocks are consistent?&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I Ask Myself Now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When was the last time a test &lt;em&gt;failed&lt;/em&gt; for a real bug?&lt;/li&gt;
&lt;li&gt;Do my integration tests actually integrate?&lt;/li&gt;
&lt;li&gt;If I removed all my mocks, how many tests would still pass?&lt;/li&gt;
&lt;li&gt;Am I measuring test coverage or test confidence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pipeline that never fails isn't reliable. It's untested.&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Monitoring Dashboard Was All Green — While 80% of Users Got Errors</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:04:45 +0000</pubDate>
      <link>https://dev.to/kollittle/my-monitoring-dashboard-was-all-green-while-80-of-users-got-errors-1o9p</link>
      <guid>https://dev.to/kollittle/my-monitoring-dashboard-was-all-green-while-80-of-users-got-errors-1o9p</guid>
      <description>&lt;h1&gt;
  
  
  My Monitoring Dashboard Was All Green — While 80% of Users Got Errors
&lt;/h1&gt;

&lt;p&gt;Everything looked perfect. Response times? Under 200ms. Error rate? 0.3%. CPU? 12%. Memory? Fine. Every metric on the dashboard was painted in soothing green.&lt;/p&gt;

&lt;p&gt;And yet, our support inbox was filling up with "the app isn't working" messages.&lt;/p&gt;

&lt;p&gt;Here's what happened, how I found it, and the monitoring lesson that changed how I think about observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;It was a Tuesday. Our app had a feature that processed user-uploaded documents — convert, OCR, store. Simple pipeline. The monitoring dashboard showed everything healthy.&lt;/p&gt;

&lt;p&gt;But users were complaining. Not loudly. Just a steady trickle of "my document didn't process" tickets.&lt;/p&gt;

&lt;p&gt;I checked the dashboard again. All green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;I dug into the raw logs — not the aggregated metrics, but the actual request-level logs. And that's when I saw it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20% of requests&lt;/strong&gt; went through the main service → responded normally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80% of requests&lt;/strong&gt; hit a load balancer rule I'd added weeks ago for "capacity management" → got silently routed to a staging queue that... nobody was consuming from&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The requests weren't failing. They were being &lt;em&gt;redirected&lt;/em&gt; to a dead end. No errors, no timeouts. Just... nothing.&lt;/p&gt;

&lt;p&gt;Our monitoring was tracking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Response time (of the 20% that completed)&lt;/li&gt;
&lt;li&gt;✅ Error rate (there were none — the requests just vanished)&lt;/li&gt;
&lt;li&gt;✅ CPU/memory (the main service was barely working, because 80% of traffic was going elsewhere)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard was green because it was only measuring the healthy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;Three weeks earlier, I'd added a load balancer rule to route "overflow" traffic to a secondary processing queue during peak loads. The idea: prevent the main service from crashing under heavy document uploads.&lt;/p&gt;

&lt;p&gt;The rule worked. It routed 80% of traffic to the secondary queue.&lt;/p&gt;

&lt;p&gt;Nobody ever set up a consumer for the secondary queue.&lt;/p&gt;

&lt;p&gt;The traffic wasn't failing. It was being &lt;em&gt;politely escorted to a room with no exit&lt;/em&gt;. And our monitoring, which only tracked the main service, had no idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Disabled the load balancer rule&lt;/strong&gt; (immediate fix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up a consumer for the secondary queue&lt;/strong&gt; (proper fix — now both paths are monitored)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added queue-depth monitoring&lt;/strong&gt; (so we catch "traffic going somewhere but not being consumed" scenarios)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Created a "silent failure" runbook&lt;/strong&gt; (a checklist for when metrics look fine but users report problems)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Green dashboards don't mean healthy systems. They mean your dashboards are measuring the things you told them to measure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The things you didn't tell them to measure? Those are the things that will quietly break everything.&lt;/p&gt;

&lt;p&gt;Here's my new rule: &lt;strong&gt;Every routing decision needs monitoring on both ends.&lt;/strong&gt; If you send traffic somewhere, you need to know if it arrives &lt;em&gt;and&lt;/em&gt; if it gets processed.&lt;/p&gt;

&lt;p&gt;A routing rule without a corresponding monitor isn't just incomplete. It's dangerous. It gives you false confidence — the worst kind of confidence in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  My "Silent Failure" Checklist
&lt;/h2&gt;

&lt;p&gt;When users report problems but your dashboard is green:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Check raw logs, not just aggregated metrics&lt;/li&gt;
&lt;li&gt;[ ] Trace a single request end-to-end (not just the happy path)&lt;/li&gt;
&lt;li&gt;[ ] Look for traffic going somewhere unexpected (new routes, old rules, load balancer configs)&lt;/li&gt;
&lt;li&gt;[ ] Check queue depths and consumer lag (requests might be waiting, not failing)&lt;/li&gt;
&lt;li&gt;[ ] Ask: "What would success look like if this was broken in a way my dashboard couldn't see?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer to that last question is usually the bug.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Your metrics are only as good as your imagination.&lt;/strong&gt; If you can't imagine how something could silently fail, you won't monitor it. And if you don't monitor it, it &lt;em&gt;will&lt;/em&gt; fail — quietly, confidently, while your dashboard stays green.&lt;/p&gt;

&lt;p&gt;What's your "green dashboard but everything is broken" story?&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Thought It Was a "Quick Fix" — That 15-Minute Change Cost Me 3 Days of Debugging</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Thu, 25 Jun 2026 22:11:00 +0000</pubDate>
      <link>https://dev.to/kollittle/i-thought-it-was-a-quick-fix-that-15-minute-change-cost-me-3-days-of-debugging-147m</link>
      <guid>https://dev.to/kollittle/i-thought-it-was-a-quick-fix-that-15-minute-change-cost-me-3-days-of-debugging-147m</guid>
      <description>&lt;p&gt;We've all said it. We've all &lt;em&gt;believed&lt;/em&gt; it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's just one line. Should take 15 minutes, max."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the story of the time I was wrong — and the checklist I now use before touching anything in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;It was Thursday afternoon. A support ticket came in: users on the free tier weren't seeing their usage stats. The paid tier worked fine. The dashboard looked broken for a chunk of our user base.&lt;/p&gt;

&lt;p&gt;I found the offending code in about 5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue was obvious — we'd recently migrated free users to a new tier label. The query was looking for &lt;code&gt;tier: 'free'&lt;/code&gt; but the new value was &lt;code&gt;'basic'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One-line fix. I typed it, ran the tests, and pushed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After (or so I thought)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;basic&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests passed. Deployed. Done. 15 minutes, start to finish.&lt;/p&gt;

&lt;p&gt;I was so proud of myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crack Appears
&lt;/h2&gt;

&lt;p&gt;By Friday morning, the dashboard was &lt;em&gt;worse&lt;/em&gt;. Not just missing stats — now free tier users were seeing &lt;strong&gt;negative usage numbers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Negative. Usage.&lt;/p&gt;

&lt;p&gt;"How?" was my first question. "Why?" was my second. "How did nobody catch this?" was my third, directed at myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;I spent Friday chasing ghosts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it a Prisma bug?&lt;/strong&gt; — No. Raw SQL queries showed the same numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a data corruption issue?&lt;/strong&gt; — No. The raw data was clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a timezone problem?&lt;/strong&gt; — I wasted 2 hours on this. It wasn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the aggregation logic wrong?&lt;/strong&gt; — Getting warmer...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what I eventually found. The change I made — adding &lt;code&gt;in: ['free', 'basic']&lt;/code&gt; — didn't just change &lt;em&gt;which&lt;/em&gt; records got fetched. It changed &lt;em&gt;how many&lt;/em&gt; records got fetched.&lt;/p&gt;

&lt;p&gt;The old code queried one table. But &lt;code&gt;basic&lt;/code&gt; tier users had their usage data split across &lt;strong&gt;two tables&lt;/strong&gt; (a migration artifact we never cleaned up). My "one-line fix" was now double-counting usage by pulling from both.&lt;/p&gt;

&lt;p&gt;The negative numbers? A subtraction later in the pipeline that assumed single-table data. Double the input → subtraction overflowed into negatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;My fix treated a &lt;strong&gt;symptom&lt;/strong&gt; (wrong tier label) as the whole problem. The real issue was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We had a half-finished data migration (two tables for one concept)&lt;/li&gt;
&lt;li&gt;The code had implicit assumptions about data shape&lt;/li&gt;
&lt;li&gt;My tests only covered the happy path with seeded data&lt;/li&gt;
&lt;li&gt;Nobody (including me) understood the full data flow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one-line change was &lt;em&gt;syntactically&lt;/em&gt; correct. It was &lt;em&gt;semantically&lt;/em&gt; wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. "Quick fixes" are the most dangerous kind
&lt;/h3&gt;

&lt;p&gt;When something feels trivial, that's exactly when you need to slow down. The pressure to "just fix it fast" is what causes production incidents.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tests don't catch what they don't know about
&lt;/h3&gt;

&lt;p&gt;My unit tests passed because they used clean, seeded data. They had no idea about the migration artifact in production. I now add this to my pre-deploy checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ Does this change touch migrated data?
□ Are there duplicate/legacy data sources?
□ What assumptions does downstream code make?
□ Have I tested with production-like data?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Read the code &lt;em&gt;around&lt;/em&gt; the change, not just the change
&lt;/h3&gt;

&lt;p&gt;I should have asked: "What happens to this data after it's fetched?" Instead, I looked at the one line, fixed it, and moved on.&lt;/p&gt;

&lt;p&gt;Now I trace the data flow at least two steps upstream and downstream before making any change.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Write down what you changed and &lt;em&gt;why&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;When you're deep in debugging at 2 AM three days later, you need context. A commit message like "fix tier label" is useless. "Update tier query to include 'basic' users — note: basic tier has legacy data in usage_v2 table" would have saved me hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix
&lt;/h2&gt;

&lt;p&gt;The actual solution wasn't a one-line change. It was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consolidate the data&lt;/strong&gt; — Write a migration script to merge the two tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the query&lt;/strong&gt; — Point it at the consolidated source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a validation test&lt;/strong&gt; — With production-like data distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the migration&lt;/strong&gt; — So the next person knows why things look the way they do&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It took a full day. But it was the &lt;em&gt;right&lt;/em&gt; fix, not the &lt;em&gt;fast&lt;/em&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  My New "Quick Fix" Checklist
&lt;/h2&gt;

&lt;p&gt;Before I touch anything in production now, I run through this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;What changed upstream?&lt;/strong&gt; (What feeds this code?)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;What changed downstream?&lt;/strong&gt; (What consumes this output?)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;What assumptions am I making?&lt;/strong&gt; (Write them down, verify each)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Are there edge cases in production data that tests don't cover?&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Would someone else understand this change from the commit message?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It adds 5 minutes to every fix. And it's saved me from at least three more multi-day debugging sessions since then.&lt;/p&gt;




&lt;p&gt;The cheapest bugs are the ones you prevent. The most expensive ones start with "it's just a quick fix."&lt;/p&gt;

&lt;p&gt;What's your worst "quick fix" story? I'd love to hear I'm not alone in this. 🙃&lt;/p&gt;

</description>
      <category>programming</category>
      <category>debugging</category>
      <category>production</category>
      <category>career</category>
    </item>
    <item>
      <title>I Swallowed My Errors for 6 Months — Then My Users Found Every Single One</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Thu, 25 Jun 2026 14:07:35 +0000</pubDate>
      <link>https://dev.to/kollittle/i-swallowed-my-errors-for-6-months-then-my-users-found-every-single-one-12fi</link>
      <guid>https://dev.to/kollittle/i-swallowed-my-errors-for-6-months-then-my-users-found-every-single-one-12fi</guid>
      <description>&lt;p&gt;I spent 6 months thinking my error handling was "good enough."&lt;/p&gt;

&lt;p&gt;I had try/catch blocks everywhere. I logged to the console. I even had a fancy Sentry dashboard.&lt;/p&gt;

&lt;p&gt;But my users were still hitting broken flows — uploading files that vanished, payments that went through without confirmation, form submissions that silently failed.&lt;/p&gt;

&lt;p&gt;The problem wasn't that I wasn't catching errors. It was that I was catching them and doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Failure Pattern
&lt;/h2&gt;

&lt;p&gt;Here's what my code looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifyUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO: handle this better&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;// TODO&lt;/code&gt; comment? It lived there for 6 months.&lt;/p&gt;

&lt;p&gt;Meanwhile, every time a file upload failed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user saw a loading spinner that never resolved&lt;/li&gt;
&lt;li&gt;Their file was never uploaded&lt;/li&gt;
&lt;li&gt;They had no idea something went wrong&lt;/li&gt;
&lt;li&gt;They tried again. And again. And gave up.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Numbers That Woke Me Up
&lt;/h2&gt;

&lt;p&gt;I finally dug into our analytics after a customer complained. Here's what I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;23% of file uploads failed silently&lt;/strong&gt; — roughly 1 in 4 uploads just disappeared&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average retry count: 2.7&lt;/strong&gt; — users tried almost 3 times before giving up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero error alerts&lt;/strong&gt; — our Sentry dashboard showed "healthy" because we caught all the errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We were running a silent disaster. Our dashboard was green, our users were bleeding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix Was Simple (But Not Easy)
&lt;/h2&gt;

&lt;p&gt;I replaced every silent catch with this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;showSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload complete!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showRetryDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showPermissionGuide&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never catch and do nothing&lt;/strong&gt; — at minimum, log it with context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the user&lt;/strong&gt; — "something went wrong" is better than infinite loading&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer a path forward&lt;/strong&gt; — retry, contact support, try a different format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categorize by error type&lt;/strong&gt; — network issues need different handling than permission issues&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;After rolling this out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silent upload failures dropped from &lt;strong&gt;23% to 0.4%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;User-reported upload issues dropped &lt;strong&gt;89%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Support tickets about "missing files" went from &lt;strong&gt;15/week to 1/week&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the biggest win wasn't the numbers. It was that when something &lt;em&gt;did&lt;/em&gt; go wrong, our team knew about it immediately instead of waiting for an angry email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;Error handling isn't about making your code not crash. It's about making failures &lt;strong&gt;visible&lt;/strong&gt; — to users and to your team.&lt;/p&gt;

&lt;p&gt;A crash is honest. A silent failure is a lie.&lt;/p&gt;

&lt;p&gt;Every time you write &lt;code&gt;catch (error) { console.log(error) }&lt;/code&gt; and move on, you're choosing to hide problems instead of solving them.&lt;/p&gt;

&lt;p&gt;The next time you catch an error, ask yourself: &lt;strong&gt;"If this fails, who needs to know — and what should they do about it?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "nobody," you probably have a bigger problem than a missing error handler.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the worst silent failure you've found in production? Drop it in the comments — I want to feel less alone.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Spent $500 on RAG Infrastructure Before Realizing These 7 Mistakes Were Killing My Results</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Sat, 20 Jun 2026 22:08:38 +0000</pubDate>
      <link>https://dev.to/kollittle/i-spent-500-on-rag-infrastructure-before-realizing-these-7-mistakes-were-killing-my-results-iph</link>
      <guid>https://dev.to/kollittle/i-spent-500-on-rag-infrastructure-before-realizing-these-7-mistakes-were-killing-my-results-iph</guid>
      <description>&lt;h1&gt;
  
  
  I Spent $500 on RAG Infrastructure Before Realizing These 7 Mistakes Were Killing My Results
&lt;/h1&gt;

&lt;p&gt;I built a RAG pipeline for private document search. It cost me $500 in vector database compute, weeks of debugging, and a lot of frustration. The results were mediocre — users got irrelevant answers, queries were slow, and the whole thing felt like a fancy keyword search with extra steps.&lt;/p&gt;

&lt;p&gt;Then I audited the pipeline step by step. Turns out, I made 7 mistakes that are incredibly common in RAG systems. Fixing them transformed the pipeline from "meh" to genuinely useful.&lt;/p&gt;

&lt;p&gt;Here's what I got wrong, and what I changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #1: I Chopped Documents Into Random Pieces
&lt;/h2&gt;

&lt;p&gt;I was splitting documents by fixed token count — 512 tokens per chunk, done. Simple, right?&lt;/p&gt;

&lt;p&gt;Wrong. I was destroying semantic context. A paragraph about API authentication would get split mid-sentence, with half in one chunk and half in another. When retrieval ran, the LLM got fragmented context and produced garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Parent-Document retrieval with semantic chunking.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split by natural document boundaries first (paragraphs, sections, headers) — these are your "parent documents"&lt;/li&gt;
&lt;li&gt;Create smaller child chunks from parents for vector search&lt;/li&gt;
&lt;li&gt;When a child chunk matches, return the full parent document to the LLM&lt;/li&gt;
&lt;li&gt;Add 10-20% overlap between chunks so boundary information isn't lost
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What I should have done from the start
&lt;/span&gt;&lt;span class="n"&gt;CHUNK_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_overlap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;。&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;！&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query accuracy jumped 30% after this one change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #2: I Used 0.5:0.5 Weights for Hybrid Search
&lt;/h2&gt;

&lt;p&gt;My vector database supports hybrid search — combining vector similarity with keyword (BM25) matching. I left the weights at the default 50/50 split and assumed that was fine.&lt;/p&gt;

&lt;p&gt;It wasn't. For technical documentation, exact keyword matches matter way more than the default acknowledges. Someone searching for "HNSW ef_construction" needs that exact term, not a semantically similar but wrong answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Dynamic weights based on query type.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Factual queries ("what is X"): 35% vector, 65% keyword&lt;/li&gt;
&lt;li&gt;Semantic queries ("how do I build X"): 75% vector, 25% keyword
&lt;/li&gt;
&lt;li&gt;General queries: 60% vector, 40% keyword
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;WEIGHTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;factual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword weight bump for factual queries alone eliminated most of the "almost right but wrong" answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #3: I Blew Up My Vector Database's Memory
&lt;/h2&gt;

&lt;p&gt;I set &lt;code&gt;ef_construction&lt;/code&gt; to the maximum value because "higher is better, right?" On a 50GB+ index, this meant the index build process consumed all available RAM and crashed. Twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Size-appropriate HNSW parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Don't max this out — your server will cry
&lt;/span&gt;&lt;span class="n"&gt;HNSW_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;# connections per node (8-32 is the sweet spot)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ef_construction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# not 400. Not 1000. 200.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ef_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# query time, not build time
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index build time went from "it crashed" to 45 minutes. Memory usage dropped 70%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #4: My Embedding Model Was Too Generic
&lt;/h2&gt;

&lt;p&gt;I was using a general-purpose embedding model trained on Wikipedia and web text. My documents were technical API references and engineering runbooks. The model didn't understand my domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Switch to a model fine-tuned for technical/code content. The difference was night and day — suddenly "migration" and "transform" weren't treated as synonyms just because they're sometimes related in general text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #5: I Had No Query Rewrite Layer
&lt;/h2&gt;

&lt;p&gt;Users typed natural questions like "why is my build slow" and the system searched for those exact words in technical documentation that said "CI pipeline optimization" and "build duration analysis." Zero overlap. Zero results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A lightweight LLM query rewrite step before retrieval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User query: "why is my build slow"
→ Rewritten: "CI pipeline performance optimization build duration"
→ Retrieved: Relevant documentation ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single step improved recall by 40%. The cost? About 0.001 cents per query with a small model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #6: I Didn't Filter Duplicate Context
&lt;/h2&gt;

&lt;p&gt;Retrieving top-10 chunks meant I often got the same paragraph 3 times with slightly different wording. The LLM would repeat itself, hallucinate from the repetition, and produce bloated answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Maximal marginal relevance (MMR) re-ranking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Instead of returning top-10 most similar
# Return top-10 most similar AND diverse
&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;diverse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mmr_rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lambda_param&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answers became more concise and covered more ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #7: I Never Measured Retrieval Quality
&lt;/h2&gt;

&lt;p&gt;I was evaluating the whole RAG pipeline end-to-end. If the final answer was bad, I didn't know if it was the retrieval, the prompt, or the LLM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Separate retrieval evaluation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track hit rate: does the retrieved context contain the answer?&lt;/li&gt;
&lt;li&gt;Track MRR (Mean Reciprocal Rank): how high in the results is the right chunk?&lt;/li&gt;
&lt;li&gt;Build a golden test set of 100 query-document pairs&lt;/li&gt;
&lt;li&gt;Only optimize the generation layer once retrieval scores are solid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This saved me from chasing the wrong problems for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results After All 7 Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Answer relevance&lt;/td&gt;
&lt;td&gt;~45%&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg query latency&lt;/td&gt;
&lt;td&gt;3.2s&lt;/td&gt;
&lt;td&gt;1.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly vector DB cost&lt;/td&gt;
&lt;td&gt;$180&lt;/td&gt;
&lt;td&gt;$95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate context in responses&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;RAG isn't hard because the algorithms are complex. It's hard because there are 7+ interconnected knobs, and they all interact with each other.&lt;/p&gt;

&lt;p&gt;My advice: fix chunking first, then weights, then embedding quality. In that order. Everything else is optimization.&lt;/p&gt;

&lt;p&gt;What's your biggest RAG headache? Drop it in the comments — I've probably hit it too.&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My API Broke Every January 1st — The Timezone Bug That Slipped Past Code Review</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Sat, 20 Jun 2026 14:04:46 +0000</pubDate>
      <link>https://dev.to/kollittle/my-api-broke-every-january-1st-the-timezone-bug-that-slipped-past-code-review-j40</link>
      <guid>https://dev.to/kollittle/my-api-broke-every-january-1st-the-timezone-bug-that-slipped-past-code-review-j40</guid>
      <description>&lt;p&gt;My API broke at exactly 00:00 UTC on January 1st. Not the users' midnight — &lt;em&gt;UTC midnight&lt;/em&gt;. Which meant our users in Tokyo had been living with broken data since 9 AM their time.&lt;/p&gt;

&lt;p&gt;And the worst part? The tests all passed. The staging environment worked fine. It only broke in production, because production is in a different timezone than staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug
&lt;/h2&gt;

&lt;p&gt;Here's what the code looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDailyReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T23:59:59Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;end&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seems fine, right? &lt;code&gt;toISOString()&lt;/code&gt; gives you UTC. We're filtering by date. What could go wrong?&lt;/p&gt;

&lt;p&gt;Here's what went wrong: &lt;code&gt;new Date(date)&lt;/code&gt; when &lt;code&gt;date&lt;/code&gt; is just &lt;code&gt;"2026-01-01"&lt;/code&gt; (no time component) gets interpreted in the &lt;strong&gt;local timezone&lt;/strong&gt;. In staging (UTC server), &lt;code&gt;"2026-01-01"&lt;/code&gt; → &lt;code&gt;2026-01-01T00:00:00.000Z&lt;/code&gt;. In production (US-East server), &lt;code&gt;"2026-01-01"&lt;/code&gt; → &lt;code&gt;2026-01-01T05:00:00.000Z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Five hour offset. Every single date query. For an entire year before anyone noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tests Passed
&lt;/h2&gt;

&lt;p&gt;Our CI runs in Docker containers set to UTC. Our staging server is also UTC. Our production server? US-East. The timezone mismatch was invisible until New Year's Day rolled around and the date boundary crossed the timezone offset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Staging (UTC):     2026-01-01 → Jan 1 00:00 UTC ✅
Production (EST):  2026-01-01 → Jan 1 05:00 UTC ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We lost 5 hours of data on every query. The reports showed numbers that were "close enough" that nobody flagged it for 12 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDailyReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Always append time to force UTC interpretation&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T00:00:00Z`&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T23:59:59.999Z`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;end&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line change. Append &lt;code&gt;T00:00:00Z&lt;/code&gt; to force the &lt;code&gt;Date&lt;/code&gt; constructor into UTC mode. No more ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix (Process, Not Code)
&lt;/h2&gt;

&lt;p&gt;The code fix took 30 seconds. The real fix took a week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Added a timezone assertion in CI&lt;/strong&gt; — our test suite now explicitly checks that &lt;code&gt;process.env.TZ === 'UTC'&lt;/code&gt;. If anyone changes the CI timezone, tests fail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;TZ=UTC&lt;/code&gt; in all Dockerfiles&lt;/strong&gt; — every container, every environment, same timezone. No surprises.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Added a timezone check to our deploy script&lt;/strong&gt; — &lt;code&gt;date +%Z&lt;/code&gt; must return &lt;code&gt;UTC&lt;/code&gt; before deploy proceeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrote a linter rule&lt;/strong&gt; — flags any &lt;code&gt;new Date(string)&lt;/code&gt; where the string doesn't contain timezone info.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;Timezone bugs are sneaky because they don't crash. They produce wrong data that looks right. Your users won't get an error page — they'll get silently incorrect numbers, and they'll trust them.&lt;/p&gt;

&lt;p&gt;Three rules I now follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never trust the system timezone.&lt;/strong&gt; Always set &lt;code&gt;TZ=UTC&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never parse dates without timezones.&lt;/strong&gt; &lt;code&gt;"2026-01-01"&lt;/code&gt; is ambiguous. &lt;code&gt;"2026-01-01T00:00:00Z"&lt;/code&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never assume your CI timezone matches production.&lt;/strong&gt; Assert it in your tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've been coding for years. I still got bit by this. If it can happen to me, it can happen to you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Read more developer war stories and technical deep-dives at &lt;a href="https://codcompass.com" rel="noopener noreferrer"&gt;codcompass.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My API Broke Every January 1st — The Timezone Bug I Should Have Caught in Code Review</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Fri, 19 Jun 2026 22:02:33 +0000</pubDate>
      <link>https://dev.to/kollittle/my-api-broke-every-january-1st-the-timezone-bug-i-should-have-caught-in-code-review-51hb</link>
      <guid>https://dev.to/kollittle/my-api-broke-every-january-1st-the-timezone-bug-i-should-have-caught-in-code-review-51hb</guid>
      <description>&lt;p&gt;My API broke at exactly 00:00 UTC on January 1st. Not the users' midnight — &lt;em&gt;UTC midnight&lt;/em&gt;. Which meant our users in Tokyo had been living with broken data since 9 AM their time.&lt;/p&gt;

&lt;p&gt;And the worst part? The tests all passed. The staging environment worked fine. It only broke in production, because production is in a different timezone than staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug
&lt;/h2&gt;

&lt;p&gt;Here's what the code looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDailyReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T23:59:59Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;end&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seems fine, right? &lt;code&gt;toISOString()&lt;/code&gt; gives you UTC. We're filtering by date. What could go wrong?&lt;/p&gt;

&lt;p&gt;Here's what went wrong: &lt;code&gt;new Date(date)&lt;/code&gt; when &lt;code&gt;date&lt;/code&gt; is just &lt;code&gt;"2026-01-01"&lt;/code&gt; (no time component) gets interpreted in the &lt;strong&gt;local timezone&lt;/strong&gt;. In staging (UTC server), &lt;code&gt;"2026-01-01"&lt;/code&gt; → &lt;code&gt;2026-01-01T00:00:00.000Z&lt;/code&gt;. In production (US-East server), &lt;code&gt;"2026-01-01"&lt;/code&gt; → &lt;code&gt;2026-01-01T05:00:00.000Z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Five hour offset. Every single date query. For an entire year before anyone noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tests Passed
&lt;/h2&gt;

&lt;p&gt;Our CI runs in Docker containers set to UTC. Our staging server is also UTC. Our production server? US-East. The timezone mismatch was invisible until New Year's Day rolled around and the date boundary crossed the timezone offset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Staging (UTC):     2026-01-01 → Jan 1 00:00 UTC ✅
Production (EST):  2026-01-01 → Jan 1 05:00 UTC ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We lost 5 hours of data on every query. The reports showed numbers that were "close enough" that nobody flagged it for 12 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDailyReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Always append time to force UTC interpretation&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T00:00:00Z`&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T23:59:59.999Z`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;end&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line change. Append &lt;code&gt;T00:00:00Z&lt;/code&gt; to force the &lt;code&gt;Date&lt;/code&gt; constructor into UTC mode. No more ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix (Process, Not Code)
&lt;/h2&gt;

&lt;p&gt;The code fix took 30 seconds. The real fix took a week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Added a timezone assertion in CI&lt;/strong&gt; — our test suite now explicitly checks that &lt;code&gt;process.env.TZ === 'UTC'&lt;/code&gt;. If anyone changes the CI timezone, tests fail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;TZ=UTC&lt;/code&gt; in all Dockerfiles&lt;/strong&gt; — every container, every environment, same timezone. No surprises.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Added a timezone check to our deploy script&lt;/strong&gt; — &lt;code&gt;date +%Z&lt;/code&gt; must return &lt;code&gt;UTC&lt;/code&gt; before deploy proceeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrote a linter rule&lt;/strong&gt; — flags any &lt;code&gt;new Date(string)&lt;/code&gt; where the string doesn't contain timezone info.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;Timezone bugs are sneaky because they don't crash. They produce wrong data that looks right. Your users won't get an error page — they'll get silently incorrect numbers, and they'll trust them.&lt;/p&gt;

&lt;p&gt;Three rules I now follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never trust the system timezone.&lt;/strong&gt; Always set &lt;code&gt;TZ=UTC&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never parse dates without timezones.&lt;/strong&gt; &lt;code&gt;"2026-01-01"&lt;/code&gt; is ambiguous. &lt;code&gt;"2026-01-01T00:00:00Z"&lt;/code&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never assume your CI timezone matches production.&lt;/strong&gt; Assert it in your tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've been coding for years. I still got bit by this. If it can happen to me, it can happen to you.&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Let AI Write My Backend Code for a Week — Here's What Actually Broke</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Sun, 14 Jun 2026 14:02:24 +0000</pubDate>
      <link>https://dev.to/kollittle/i-let-ai-write-my-backend-code-for-a-week-heres-what-actually-broke-1d36</link>
      <guid>https://dev.to/kollittle/i-let-ai-write-my-backend-code-for-a-week-heres-what-actually-broke-1d36</guid>
      <description>&lt;p&gt;I told myself it would be fine. I had been using AI coding assistants for suggestions and autocomplete for months — and it worked great. So when a new project came up with a tight deadline, I thought: why not let AI handle the whole backend?&lt;/p&gt;

&lt;p&gt;I set up a Cursor workspace, wrote a detailed spec, and hit generate. What followed was 5 days of "it compiles, but..." debugging that taught me more about software engineering than any tutorial ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Surprisingly Well
&lt;/h2&gt;

&lt;p&gt;The boilerplate was genuinely impressive. In about 2 hours, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fully typed Express.js API with 12 endpoints&lt;/li&gt;
&lt;li&gt;Zod validation schemas for every route&lt;/li&gt;
&lt;li&gt;A Prisma schema with proper relations&lt;/li&gt;
&lt;li&gt;Docker compose setup with Postgres and Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code looked clean. Tests passed. I was feeling like a 10x developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cracks Started Showing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug #1: Silent Type Coercion
&lt;/h3&gt;

&lt;p&gt;The AI generated this validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right? Except the API received ages as strings from the frontend. Zod parsed them fine in development (coercion worked). But in production with stricter mode? &lt;code&gt;NaN&lt;/code&gt; everywhere. Users were getting 400 errors on signup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;z.coerce.number().int().positive()&lt;/code&gt; — but I had to find all 23 instances manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #2: The N+1 Query Nobody Asked For
&lt;/h3&gt;

&lt;p&gt;For a dashboard endpoint that listed users with their orders and order items, the AI generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &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;user&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classic N+1. The Prisma docs literally have a page titled "How to avoid N+1 queries." With 500 users, this endpoint made 501 database queries and took 8 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;include&lt;/code&gt; with nested relations — one query, 120ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #3: Race Conditions in Token Refresh
&lt;/h3&gt;

&lt;p&gt;The AI wrote a token refresh flow that looked perfect in isolation. But under load, concurrent refresh requests would invalidate each other's tokens. The AI's solution? "Add a retry mechanism." My solution? "Use a refresh token rotation pattern that handles concurrency properly."&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #4: The Error Handler That Swallowed Everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;console.log&lt;/code&gt; doesn't serialize Error objects properly. Every production error was just &lt;code&gt;{}&lt;/code&gt; in the logs. We ran like this for 3 days before anyone noticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;console.error&lt;/code&gt; with proper error serialization and a proper logging library (we went with Pino).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem
&lt;/h2&gt;

&lt;p&gt;Here's what I learned: &lt;strong&gt;AI generates code that's correct in isolation but fragile in context.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It doesn't know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your deployment architecture (so it misses N+1 queries)&lt;/li&gt;
&lt;li&gt;Your traffic patterns (so it ignores race conditions)&lt;/li&gt;
&lt;li&gt;Your logging infrastructure (so it uses the wrong logger)&lt;/li&gt;
&lt;li&gt;Your team's conventions (so it mixes patterns)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated code passes tests because tests are narrow. It compiles because the syntax is valid. But production is where context matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Changed
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI writes the first draft, humans write the final version.&lt;/strong&gt; I'm not going back to writing everything from scratch, but every PR now requires a manual review of control flow, error handling, and data access patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecture decisions stay human.&lt;/strong&gt; Schema design, caching strategy, and error handling patterns are too context-dependent to outsource.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add integration tests that AI can't fake.&lt;/strong&gt; Unit tests pass. Integration tests reveal the gaps. We added a test suite that runs the full API against a real Postgres instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Observability from day one.&lt;/strong&gt; Structured logging, request tracing, and error tracking are now part of the project template, not an afterthought.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI didn't break my project. My assumption that "generated code equals production-ready code" did.&lt;/p&gt;

&lt;p&gt;AI is an incredible force multiplier when used as a pair programmer. It's a liability when treated as a replacement for engineering judgment.&lt;/p&gt;

&lt;p&gt;The week cost me 3 extra days of debugging, but I shipped a more robust system than I would have built alone — because the AI's mistakes taught me where my own blind spots were.&lt;/p&gt;

&lt;p&gt;Use AI. But keep your hands on the wheel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you had similar experiences with AI-generated code? I'd love to hear your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Our Test Suite Passed 100% — Then Users Found 14 Bugs in One Day</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Tue, 09 Jun 2026 18:03:24 +0000</pubDate>
      <link>https://dev.to/kollittle/our-test-suite-passed-100-then-users-found-14-bugs-in-one-day-5o0</link>
      <guid>https://dev.to/kollittle/our-test-suite-passed-100-then-users-found-14-bugs-in-one-day-5o0</guid>
      <description>&lt;p&gt;We had 847 tests. Green checkmarks across the board. 100% coverage on our critical paths. I was proud of that dashboard.&lt;/p&gt;

&lt;p&gt;Then a user reported that our checkout was double-charging on Safari. Another said the password reset emails weren't arriving. Within 24 hours we had 14 confirmed bugs — and our CI pipeline was still proudly green.&lt;/p&gt;

&lt;p&gt;That's when I realized: &lt;strong&gt;100% code coverage is a vanity metric that makes you feel safe while your users burn.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Coverage
&lt;/h2&gt;

&lt;p&gt;Here's what our test suite was great at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing individual functions in isolation&lt;/li&gt;
&lt;li&gt;Verifying happy paths with clean inputs&lt;/li&gt;
&lt;li&gt;Catching regressions in pure utility functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what it completely missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser-specific behavior&lt;/strong&gt; — Safari's date parsing is different from Chrome's. Our test runner used Node.js. No browser, no Safari.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions&lt;/strong&gt; — Two API calls firing simultaneously? Our mocked fetch resolved instantly. In production, timing matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration gaps&lt;/strong&gt; — Each module had tests. The &lt;em&gt;connections&lt;/em&gt; between modules did not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world data&lt;/strong&gt; — Our fixtures were clean. User data is never clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bug That Started It All
&lt;/h2&gt;

&lt;p&gt;A user in Japan reported being charged twice for a single purchase. We couldn't reproduce it locally. Our payment integration tests passed every time.&lt;/p&gt;

&lt;p&gt;The root cause: a double-submit button on slow networks. Our mock API responded in 12ms. Real networks: 800ms. That gap was enough for impatient fingers to click twice.&lt;/p&gt;

&lt;p&gt;The fix was 3 lines of code:&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;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsSubmitting&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;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Button: disabled={isSubmitting}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. But the test suite — our beautiful 847-test suite — had zero tests for this scenario because &lt;strong&gt;nobody wrote a test for "user clicks button twice."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 14-Bug Autopsy
&lt;/h2&gt;

&lt;p&gt;After that incident, we categorized all 14 bugs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Tests Should've Caught It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser compatibility&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;❌ No cross-browser tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Race conditions&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;❌ Mocks too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge-case user input&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;❌ Fixtures too clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party API changes&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;❌ No contract testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time zone bugs&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;❌ All tests ran in UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;14 bugs. Zero caught by CI. The problem wasn't that we didn't have enough tests — &lt;strong&gt;we had the wrong kind of tests.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Changed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Added Integration Tests at Module Boundaries
&lt;/h3&gt;

&lt;p&gt;Unit tests check the bricks. Integration tests check the mortar. We added tests specifically for the &lt;em&gt;connections&lt;/em&gt; between services — where most real bugs hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Started Running Tests in Real Browsers
&lt;/h3&gt;

&lt;p&gt;We added Playwright for critical user flows: checkout, auth, search. These run against a real Chrome and Firefox instance. Safari is next.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Mock Network Latency
&lt;/h3&gt;

&lt;p&gt;Instead of instant mock responses, we randomized delays between 100ms and 2000ms. This surfaced race conditions we never knew existed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Contract Testing for APIs
&lt;/h3&gt;

&lt;p&gt;We used Pact to verify that our frontend's expectations of backend APIs actually match reality. Two bugs disappeared the day we added this.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Time Zone Roulette
&lt;/h3&gt;

&lt;p&gt;We randomize the test runner's timezone. Half our date bugs appeared within the first week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Philosophy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coverage tells you what code runs. It doesn't tell you what breaks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we track different metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bug escape rate&lt;/strong&gt; — bugs found by users vs. caught in CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mean time to detection&lt;/strong&gt; — how fast our tests find regressions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration test coverage&lt;/strong&gt; — not line coverage, but &lt;em&gt;scenario&lt;/em&gt; coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our total test count went down (we deleted 200+ redundant unit tests). Our bug escape rate went down 80%.&lt;/p&gt;

&lt;p&gt;The dashboard looks less impressive. The product works better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you been burned by "green tests, broken production"? What testing gaps surprised you most? I'd love to hear your war stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Added 20 Indexes to "Fix" Slow Queries — My Database Got 3x Slower</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Mon, 08 Jun 2026 14:02:21 +0000</pubDate>
      <link>https://dev.to/kollittle/i-added-20-indexes-to-fix-slow-queries-my-database-got-3x-slower-3ac2</link>
      <guid>https://dev.to/kollittle/i-added-20-indexes-to-fix-slow-queries-my-database-got-3x-slower-3ac2</guid>
      <description>&lt;h1&gt;
  
  
  I Added 20 Indexes to "Fix" Slow Queries — My Database Got 3x Slower
&lt;/h1&gt;

&lt;p&gt;Six months ago, I inherited a PostgreSQL database that was choking on production traffic. API response times hit 8 seconds. Users were timing out. The ops team was getting paged at 2 AM.&lt;/p&gt;

&lt;p&gt;So I did what any "experienced" developer would do: I added indexes. Lots of them.&lt;/p&gt;

&lt;p&gt;Twenty indexes across twelve tables. Problem solved, right?&lt;/p&gt;

&lt;p&gt;Wrong. The database got &lt;strong&gt;slower&lt;/strong&gt;. Write operations crawled. Disk usage spiked. And the queries I was trying to optimize? They were still slow.&lt;/p&gt;

&lt;p&gt;Here's what I learned the hard way about index tuning — and the process I use now that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake Everyone Makes
&lt;/h2&gt;

&lt;p&gt;The biggest misconception about indexes is this: &lt;em&gt;more indexes = faster queries&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL has to maintain every index on every write. Add an index, and every INSERT, UPDATE, and DELETE gets heavier. With 20 extra indexes, our write-heavy analytics table was spending more time updating indexes than storing data.&lt;/p&gt;

&lt;p&gt;But the real killer was something I didn't expect: &lt;strong&gt;index bloat&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Went Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I Indexed Low-Cardinality Columns
&lt;/h3&gt;

&lt;p&gt;I put an index on a &lt;code&gt;status&lt;/code&gt; column with only 4 possible values: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;suspended&lt;/code&gt;, &lt;code&gt;deleted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL's query planner looked at that index, saw that each value matched ~25% of rows, and decided a full table scan was cheaper. The index was dead weight — costing disk space and write performance, providing zero read benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I Created Redundant Indexes
&lt;/h3&gt;

&lt;p&gt;I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CREATE INDEX idx_user_email ON users(email)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE INDEX idx_user_email_name ON users(email, name)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second index already covers queries on &lt;code&gt;email&lt;/code&gt; alone. The first one was pure redundancy. PostgreSQL was maintaining two indexes for essentially the same lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. I Ignored Partial Indexes
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;orders&lt;/code&gt; table had millions of rows, but 90% were &lt;code&gt;completed&lt;/code&gt; or &lt;code&gt;cancelled&lt;/code&gt;. The slow queries were all looking for &lt;code&gt;status = 'pending'&lt;/code&gt;. A partial index like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_pending&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tiny index (10% of the table) outperformed my full-table indexes by 5x.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A Methodical Index Audit
&lt;/h2&gt;

&lt;p&gt;Here's the process I followed to undo the damage and actually optimize:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Find Unused Indexes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;indexname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;idx_scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;index_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;idx_scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;indisunique&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revealed 14 indexes that had &lt;strong&gt;never been used&lt;/strong&gt; since the last stats reset. I dropped them immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Find the Real Slow Queries
&lt;/h3&gt;

&lt;p&gt;Instead of guessing, I used &lt;code&gt;pg_stat_statements&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_exec_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This showed me which queries were actually burning CPU time. Not the ones I assumed were slow — the ones that &lt;em&gt;actually were&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Use EXPLAIN ANALYZE
&lt;/h3&gt;

&lt;p&gt;For every slow query, I ran &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; to see the actual execution plan. Not &lt;code&gt;EXPLAIN&lt;/code&gt; — &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. The difference is that &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually runs the query and shows real timing data.&lt;/p&gt;

&lt;p&gt;What I found: PostgreSQL was doing sequential scans on tables where I had indexes, because my query conditions didn't match the index column order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Build Right-Sized Indexes
&lt;/h3&gt;

&lt;p&gt;The three indexes that actually made a difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Composite index matching the actual WHERE + ORDER BY pattern&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_analytics_date_type&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Covering index that includes all needed columns (no table lookup)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_lookup&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Expression index for a common pattern&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_lower_email&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before Audit&lt;/th&gt;
&lt;th&gt;After Audit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total indexes&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg query time&lt;/td&gt;
&lt;td&gt;3.2s&lt;/td&gt;
&lt;td&gt;0.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write latency&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index disk usage&lt;/td&gt;
&lt;td&gt;12.4 GB&lt;/td&gt;
&lt;td&gt;2.1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index cache hit rate&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Rule I Follow Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never add an index without running EXPLAIN ANALYZE first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every index should have a specific query it's designed to accelerate. If you can't point to the query and show the before/after execution plan, don't create the index.&lt;/p&gt;

&lt;p&gt;Indexes are not a "just in case" thing. They're a surgical tool. Use them like one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you ever made your database slower by trying to optimize it? What was your wake-up call?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Thought My API Was Rate-Limited — Until Someone Scraped 2 Million Requests in 4 Hours</title>
      <dc:creator>kol kol</dc:creator>
      <pubDate>Sun, 07 Jun 2026 14:04:53 +0000</pubDate>
      <link>https://dev.to/kollittle/i-thought-my-api-was-rate-limited-until-someone-scraped-2-million-requests-in-4-hours-509d</link>
      <guid>https://dev.to/kollittle/i-thought-my-api-was-rate-limited-until-someone-scraped-2-million-requests-in-4-hours-509d</guid>
      <description>&lt;p&gt;I had &lt;code&gt;express-rate-limit&lt;/code&gt; installed. I had it configured. I had tests that proved it worked.&lt;/p&gt;

&lt;p&gt;And yet, someone still scraped 2 million API requests from my production server in under 4 hours. Costing me $4,200 in upstream API calls.&lt;/p&gt;

&lt;p&gt;Here's exactly what went wrong, how I found out, and the architecture I use now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup That Lied to Me
&lt;/h2&gt;

&lt;p&gt;My API was a simple Express app. I added rate limiting like any reasonable developer would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express-rate-limit&lt;/span&gt;&lt;span class="dl"&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;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;// 100 requests per window&lt;/span&gt;
  &lt;span class="na"&gt;standardHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;legacyHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests passed. I saw &lt;code&gt;X-RateLimit-Limit: 100&lt;/code&gt; in curl responses. I slept well.&lt;/p&gt;

&lt;p&gt;The problem? I was running &lt;strong&gt;4 instances&lt;/strong&gt; behind a load balancer. Each instance had its own in-memory counter. So the real limit was &lt;strong&gt;400 requests per 15 minutes&lt;/strong&gt; — not 100.&lt;/p&gt;

&lt;p&gt;And the attacker wasn't hitting one IP with 100 requests. They were rotating through a proxy pool of 2,000+ IPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Happened
&lt;/h2&gt;

&lt;p&gt;At 2:47 AM, our monitoring dashboard showed something odd: API request volume spiked 800%. I dismissed it as a newsletter push going out.&lt;/p&gt;

&lt;p&gt;By 4:00 AM, the database connection pool was saturated. Queries that normally took 12ms were timing out at 30 seconds.&lt;/p&gt;

&lt;p&gt;By 6:30 AM, I checked our upstream LLM provider bill. We'd made &lt;strong&gt;2.1 million API calls&lt;/strong&gt; since midnight. At $0.002 per call, that's roughly &lt;strong&gt;$4,200&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The attacker was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hitting our search endpoint with systematic keyword variations&lt;/li&gt;
&lt;li&gt;Rotating IPs from a residential proxy network&lt;/li&gt;
&lt;li&gt;Staying under per-instance rate limits by spreading requests across IPs&lt;/li&gt;
&lt;li&gt;Extracting structured data from our responses&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why My Defenses Failed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Defense&lt;/th&gt;
&lt;th&gt;Why It Failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;express-rate-limit&lt;/code&gt; (in-memory)&lt;/td&gt;
&lt;td&gt;Not shared across instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP-based limiting&lt;/td&gt;
&lt;td&gt;Proxy rotation defeated it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No request logging depth&lt;/td&gt;
&lt;td&gt;Couldn't trace the attack pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No anomaly alerts&lt;/td&gt;
&lt;td&gt;800% spike looked like "normal traffic"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fundamental mistake: I treated rate limiting as a &lt;strong&gt;configuration problem&lt;/strong&gt; instead of an &lt;strong&gt;architecture problem&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Distributed Rate Limiting
&lt;/h2&gt;

&lt;p&gt;I rebuilt the system with three layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Redis Sliding Window (The Real Rate Limiter)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis-rate-limiter&lt;/span&gt;&lt;span class="dl"&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;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;windowSec&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;windowSec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Use Redis sorted set for true sliding window&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zremrangebyscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;windowStart&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zcard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;max&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="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;windowSec&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="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&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 gives you a &lt;strong&gt;true&lt;/strong&gt; 100-request limit across all instances, not 100 per instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Behavioral Fingerprinting
&lt;/h3&gt;

&lt;p&gt;IP addresses are useless against proxy pools. Instead, I track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request pattern entropy&lt;/strong&gt; — Are endpoints being hit in alphabetical order? That's a scraper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing regularity&lt;/strong&gt; — Requests every exactly 1.0 seconds? Bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header consistency&lt;/strong&gt; — Same User-Agent, same Accept-Encoding, same everything? Bot.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateRequestEntropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requests&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;endpoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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;uniqueEndpoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Low entropy = sequential/scraping pattern&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;uniqueEndpoints&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Entropy &amp;lt; 0.3 → likely scraping&lt;/span&gt;
&lt;span class="c1"&gt;// Entropy &amp;gt; 0.7 → likely human&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 3: Cost-Based Circuit Breakers
&lt;/h3&gt;

&lt;p&gt;This is the one that actually saves money:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Track estimated cost per endpoint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpointCosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// LLM call&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/analyze&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Expensive LLM call&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Cheap&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hourlyCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;COST_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Alert at $50/hr&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hourlyCost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;endpointCosts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hourlyCost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;COST_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Auto-throttle expensive endpoints&lt;/span&gt;
    &lt;span class="nx"&gt;expensiveEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API cost spike: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hourlyCost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/hr`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When costs spike, expensive endpoints automatically throttle. You don't need to be awake at 3 AM to stop a bleeding wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results After 30 Days
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Successful scrapes&lt;/td&gt;
&lt;td&gt;2 incidents&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak API cost/hr&lt;/td&gt;
&lt;td&gt;$4,200&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive blocks&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2 (tuned rules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legitimate user impact&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;None detected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;Rate limiting isn't about setting a number. It's about understanding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your threat model&lt;/strong&gt; — Who would want to scrape your API and why?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your architecture&lt;/strong&gt; — In-memory doesn't work in a distributed system. Period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your cost exposure&lt;/strong&gt; — Know the dollar cost per endpoint, and set automatic circuit breakers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The $4,200 mistake taught me that &lt;strong&gt;security theater&lt;/strong&gt; — rate limiting that looks right but isn't — is worse than no rate limiting at all. It gives you confidence to deploy things that aren't actually protected.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you ever been bitten by a "working" defense that wasn't? What's your rate limiting setup? Drop it in the comments — I'm always looking for ways to improve mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codcompass</category>
      <category>ai</category>
      <category>knowledgebase</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
