{
"$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"
}