The free-tier trap: the cache that almost DoS'd my own app
I added a cache to save money. It nearly took the app down, and the alert looked like an attack. A postmortem on reading the fine print.
We spent a weekend making a free, public app cheaper to run, and by Monday it was briefly unusable. Not because of an attacker. Because of me, an incremental cache, and a free-tier limit I never read to the end of.
This is the postmortem I wish someone had published before I shipped it.
The setup
The app is a nonprofit audio library: roughly fifteen hundred pages, all public, all cacheable. Rendering each page hit the database, so the obvious win was an incremental cache in front. The platform offered a key-value store on a generous free tier, and “generous” is exactly the word that got me.
I wired the cache to write a rendered entry on every miss. Deploy, smoke test, walk away.
A free tier is a budget, not a guarantee. The numbers are the spec.
The incident
A search crawler discovered the sitemap that night and walked all fifteen hundred pages in a few minutes. Every one was a cache miss. Every miss was a write.
The key-value store’s free tier allows one thousand writes per day. The crawl blew through that before it finished the letter B. Writes started failing, the render path treated a failed cache write as a hard error, and pages began returning fives. The platform’s “daily limit exceeded” email landed in my inbox at 2 a.m. and read, for all the world, like the first sign of an attack.
It was not an attack. It was arithmetic.
What actually went wrong
Three mistakes stacked:
1. wrong backend — KV is built for read-heavy, write-rare config,
not for caching a 1,500-page catalog.
2. wrong failure — a failed cache WRITE should never fail the READ.
3. wrong mental model — "free" read as "unlimited," never as "1,000/day."
The fix was not a bigger plan. It was moving the cache to object storage, whose free tier measures writes in the millions per month, and making the write fire-and-forget so a throttled cache can never again take down a page it was supposed to speed up.
The lesson worth the outage
The failure that scared me was not the outage. It was how convincingly a self-inflicted cost problem impersonated a security event. If I had reflexively started blocking traffic, I would have been fighting my own crawler.
Read the limit before you optimize against it. And when a cache fails, it should get quieter, never louder.