How Vibe Coding Cost Me My Free Tier
For a while I was running simmr’s database locally on a Raspberry Pi. It worked fine, but the gap between that and production was starting to bother me. Neon has a neat feature where you can branch your production database. You get a copy-on-write fork that shares storage with prod, which means your dev environment is running against real schema and seed data without paying for a second database. So I switched to that.
What I didn’t think about was what I left running.
The quota error
A few days later I sat down to work on simmr and hit a compute quota error. My first thought was that usage had picked up. Maybe I’d finally gotten some real traffic. I opened the Neon dashboard and that was not it. My dev branch was responsible for most of the compute hours. The app had no users hitting it in dev. Something was keeping the database warm around the clock.
The outbox
Earlier, during a particularly productive vibe-coding session, I’d landed on an event processing architecture that used an outbox table. The idea is solid in the right context: write events to a table inside your main transaction, then have a background process poll and flush them to downstream consumers. It decouples your writes from your event delivery and gives you at-least-once guarantees for free.
The problem was that I had a poller running locally that flushed the outbox every few minutes. It was hitting the dev branch continuously, which meant Neon never saw a window of inactivity. The database could never autosuspend.
The same thing was happening on the prod branch for the same reason. The poller kept it awake even when no real traffic existed. I was paying for two always-on databases when the whole point of Neon’s free tier is scale-to-zero.
The real problem
Once I stepped back, the outbox system was obviously over-engineered for my use case. Simmr is a solo project with no real traffic yet. I don’t need at-least-once delivery guarantees. I don’t need decoupled event processing. I need a database that goes to sleep when I’m not using it so I can stay on the free tier and keep my cloud costs at zero while I build.
Vibe coding got me a pretty cool architecture. It also got me a pretty silly bill.
What I did
I dropped the outbox system entirely. No more background poller, no more periodic flushes, no more invisible compute usage. Events that need to happen just happen inline. If simmr grows to the point where I need decoupled event processing, I’ll add it back deliberately, but I’ll do it knowing what it costs.
With the poller gone, both branches started autosuspending within minutes of inactivity. Compute usage dropped to near zero.
What I took away from this
Scale-to-zero is a behavior you have to design for. It’s not enough to pick a provider that supports autosuspend. You have to make sure nothing in your stack is holding the door open. Background loops, connection pools, health checks, anything that touches the database on a timer.
Vibe coding is great for exploration, bad for cost awareness. When you’re moving fast and the AI is generating interesting architectures, it’s easy to land on patterns that are correct in the abstract but wrong for your actual constraints. An outbox is a real pattern. It just wasn’t my pattern.
Dev environments inherit prod behavior. When I branched the database, the dev branch inherited the same always-on poller. That’s the kind of thing that doesn’t show up in your code review because the code is doing exactly what it was designed to do, just in a context where you don’t want it to.
If you’re building on Neon or any managed database with autosuspend, take five minutes and check what’s keeping your database awake. You might be paying for compute you’re not using.