On 26 July 2026 Cloudflare sent me a polite email to let me know I was spending more than double what I was allowed. The public dashboard for the volunteer fire department command had been saying the same thing in its own way, with less courtesy: it went down every so often. A badly placed cache was writing on every visit, 2,400 writes a day against a limit of 1,000 a day.
On 25 July, one day earlier, we had moved that site off the VPS onto the free plan and I was sleeping well: no server to patch, no cron job on a machine somebody has to remember to restart. How much we were consuming, I had no idea.
Every resource in the plan has its own limit
I thought of the free plan as one single tank, and that was my mistake. They are separate pockets, each with its own limit: KV writes (the small key and value store, 1,000 a day), Worker invocations, rows read in D1, R2 storage, Workers AI usage, cron triggers and GitHub Actions minutes.
It works like a prepaid phone plan with separate balances. You run out of data and the phone stops telling you where your combi is (the minibus everyone takes across Lima), even with all your call minutes still sitting there. Running out in one pocket takes the site down while the others sit nearly empty. We blew the smallest one of all.
The dashboard going down every so often
Me sleeping fine with no server to patch
The meter runs every day at the same hour
A script with no dependencies runs once a day on GitHub Actions: it reads the real usage of each resource and compares it against its free limit. From 70% up it warns, and past 90% it fails the build. In the same run it checks that the pages respond and that the dashboard data is fresh.
We had tried a daily email in red for a problem no automation could fix, and in a week all of us learned to ignore it. Then we tried the opposite, a quiet warning with the build still green, which hid the quota problem for days. Now the status lives in a GitHub issue that gets updated, instead of a new email every morning.
The quota stayed at 756 after my guess
With the cache fixed, the meter left projected KV writes at 756 of 1,000 a day. The explanation took me two seconds: we were missing conditional writes, writing only when the value changes. We went to the code and they had been implemented all along. Guessing cost me an afternoon, and the quota stayed at 756, just as firm.
Two keys nobody ever asked for
We put a meter on each pipe: one log line for every write to KV, and we let the site run for 19 minutes to see which one was ticking. Two internal bookkeeping keys showed up on that list, the cursors that mark how far each scraper got, and they took 30% of the daily quota. No browser ever reads them.
We moved the two cursors to D1, the SQL database on the same plan, which allows 100,000 writes a day. The same work went from 30% of the small quota to 0.3% of the big one. We left a test in CI, the automated review that runs with every change, that fails if a cursor goes back to KV, so nobody has to remember the rule.
What the meter costs and what it doesn't solve
The meter also spends from a pocket: about 2 minutes per run, close to 60 of the 2,000 free Actions minutes a month. By that same arithmetic, the Instagram scraper now runs every 30 minutes instead of every 15, and it uses around 1,440 minutes a month, 72% of the quota. That's the tightest pocket we have left and I don't see any air in it.
The meter only warns. If dashboard traffic grows for good, no script is going to save us: we pay, or we move the work to another pocket.
When something consumes more than it should, I look first at which meter is ticking, and we don't migrate anything without its log line. Cloudflare hasn't written to me again, and the dashboard isn't complaining either.
