BlogTokens & Costs

Free with a meter

A cache spent 2,400 writes a day on a plan that allowed 1,000

Nicolás Biondi

Five copper pipes, each with a ring of cyan light; from one of them a thread of amber light drips down and forms a puddle.

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.

Meme Disaster Girl: The dashboard going down every so often / Me sleeping fine with no server to patch 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.

before

now

Scraper cursors

Where they write

KV: 1,000 a day

D1: 100,000 a day

30 percent used

0.3 percent used

The same two scraper cursors, before in the small quota and now in the big one.

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.

cloudflare quotas caching metrics postmortem