{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifxflz4ivqw7givcsqff72lv2ouuuoth4cj2wiazi7fbuwyvf2rbm",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mox4fl3gevj2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreibotpapw7kowjupomusjaurvqvehhqqkwggk2za3srvbe35okfj6u"
    },
    "mimeType": "image/webp",
    "size": 64914
  },
  "path": "/manolito99/the-nodejs-mistake-that-cost-my-client-3000-in-aws-bills-30a5",
  "publishedAt": "2026-06-23T09:37:15.000Z",
  "site": "https://dev.to",
  "tags": [
    "node",
    "javascript",
    "aws",
    "webdev"
  ],
  "textContent": "Last year I was asked to investigate a startup's AWS bill.\n\nIt had jumped from roughly $200/month to over $3,000 in a few weeks.\n\nNobody knew why.\n\nAfter digging through logs, metrics, and database traffic, I found the culprit: a polling loop with no backoff strategy.\n\nThe code looked harmless:\n\n\n\n    async function processQueue() {\n      const jobs = await getJobs()\n\n      for (const job of jobs) {\n        await processFile(job)\n      }\n\n      processQueue()\n    }\n\n    processQueue()\n\n\nAt first glance, this seems reasonable. Process all available jobs, then check again.\n\nThe problem appears when the queue is empty.\n\nWhen `getJobs()` returned no work, the loop immediately queried the database again. And again. And again.\n\nThere was no delay, no backoff, and no event-driven trigger.\n\nAs a result, the service continuously hammered the database looking for work that didn't exist.\n\nEach iteration generated:\n\n  * A database query\n  * Network traffic\n  * CPU usage\n  * Logging overhead\n  * Additional infrastructure load\n\n\n\nIndividually, each operation was cheap.\n\nExecuted hundreds of thousands of times per day, they became expensive.\n\nThe fix was simple:\n\n\n\n    async function processQueue() {\n      while (true) {\n        const jobs = await getJobs()\n\n        for (const job of jobs) {\n          await processFile(job)\n        }\n\n        await new Promise(resolve => setTimeout(resolve, 5000))\n      }\n    }\n\n\nEven better would have been replacing polling entirely with an event-driven design using a message queue.\n\nWhat this incident taught me:\n\n**1. Empty queues are production workloads.**\n\nMany engineers optimize for peak traffic and forget about idle traffic. Systems often spend more time idle than busy.\n\n**2. Polling needs backoff.**\n\nIf you're polling, always define what happens when no work is found.\n\n**3. Cost bugs rarely look like bugs.**\n\nNothing crashed. No exceptions were thrown. The system was technically working exactly as written.\n\nIt was just doing useless work 24/7.\n\n**4. Always monitor cost alongside performance.**\n\nCPU, latency, and error rates looked normal.\n\nThe AWS bill was the first real alert.\n\nOne question I ask during reviews now:\n\n**\"What does this code do when there's nothing to do?\"**\n\nThat single question has caught more production issues than many architecture discussions ever did.\n\nWhat's the most expensive bug you've ever seen in production?",
  "title": "The Node.js Mistake That Cost My Client $3,000 in AWS Bills"
}