← Back to the tea house
← LEDGER
AO-072 / Oct 28, 2026

My media library hit Cloudflare's free database limit in four days

My agents moved my 68,000-file media library into a Cloudflare D1 database, and on the fourth day Cloudflare cut off every database on my account. Why it happened, and what to set up first.

My agents moved my media library into a Cloudflare database on 18 October, and on the fourth day it went over the free plan’s daily limit. Cloudflare then refused every database query on my account until midnight. The database counts every row a query (a question to the database) looks through, not just the rows it hands back, and a few of the library’s queries looked through the whole table every time. Some ran on timers. One page read all 68,000 rows on every load.

The library is where everything my agents make gets filed for me to review, about 68,000 files. It moved off my laptop into my command center, the private dashboard my agents post to. That runs on Cloudflare with a D1 database behind it. D1 is Cloudflare’s hosted SQLite, a small and very common database. Each file is a row, and the free plan lets a whole account read 5 million rows a day. In early October mine read about 46,000 a day. From the day the library moved in, it read 2.4 to 5.3 million.

DayRows read
2026-10-0144637
2026-10-0243563
2026-10-0345431
2026-10-0446881
2026-10-0556320
2026-10-0649302
2026-10-0748010
2026-10-0846948
2026-10-0946729
2026-10-1046829
2026-10-1145678
2026-10-1243741
2026-10-1340154
2026-10-1443976
2026-10-1543798
2026-10-1645026
2026-10-1724508
2026-10-182948898
2026-10-193091746
2026-10-204672589
2026-10-215273527
2026-10-223020763
2026-10-232408027
2026-10-245249487
2026-10-254354757
2026-10-264974670
2026-10-275173282
  1. The library goes live
  2. Over the limit: D1 refuses queries from about 21:00 UTC
  3. Over the limit at 23:54 UTC
  4. Over at 18:47 UTC: every query refused from 19:00 to midnight UTC
Rows read per day by my Cloudflare account, 1 to 27 October 2026Every D1 database on the account, from Cloudflare's own analytics. The free plan allows 5,000,000 a day in total

It went over three times. On the 27th the account read 5.17 million rows, about the whole library 75 times over, and every query was refused for the last five hours of Cloudflare’s day. The limit covers the whole account, so this blog’s small database got refused too. The pages stayed up because they’re plain files. Only the numbers behind the stats page broke.

What I’d do instead

  • Take the $5 plan before you move anything big in. Cloudflare’s Workers Paid plan has no daily cutoff and includes 25 billion rows read a month. My wasteful average of 4.1 million a day is 0.5% of that. I took it after the third day over.
  • On a full-size copy, ask the database how it plans to answer every query that runs often. EXPLAIN QUERY PLAN shows the plan without running the query. SCAN means every row, even when it also says USING INDEX. An index is a sorted lookup list, like the one at the back of a book. SQLite picks its own plan, and INDEXED BY makes it use the index you name. D1 also returns how many rows each query read (meta.rows_read).
  • Never put a whole-table query on a timer. Keep running counts in their own table, and have background jobs ask only about what changed.
  • Count reads like you count writes. The library budgeted every write and never counted a read, so ten days at an average of 82% of the limit went by unnoticed.

Where the 5 million went

Claude went through the 27th from the logs and D1’s own analytics, then fixed every query. Here’s what used the most that day, and what one call read before and after:

  • The library page’s summary: 2.6 million, half the day. It counted the 531 files agents had picked, with an index made just for those. SQLite ignored it and read all 68,000 rows every time. The page ran it on every load and every two minutes while it was open, and two laptop jobs ran it every hour. INDEXED BY took it from 68,687 rows to 1,062.
  • The sweeper and other background jobs: 1 million. The sweeper files new uploads. Every 15 minutes it looked up every recent one again, even ones it had filed days before. Asking only about new ones took a run from about 11,600 rows to under 200.
  • My agents’ search for one job’s files: 0.7 million. Nothing indexed the column it searches. With an index, a one-file job went from 68,156 rows to 2.
  • A laptop job listing the files I’ve loved: 0.4 million. It asked for 200 at a time and only 108 are loved, so it never got its 200 and searched to the end of the table. Sorted by an index of loved files, it went from 68,156 rows to 108.

After the fixes

Everything went live early on 28 October. The live library summary now reads 1,831 rows instead of about 70,000. The account should settle near 0.6 to 0.7 million rows a day, about a sixth of what it was.

The exact times and how each number was measured
  • The account went over the limit at about 21:00 UTC on 21 October, at 23:54 UTC on 24 October and at about 18:47 UTC on 27 October. On the 27th every query was refused from 19:00 to midnight UTC.
  • Since 1 September, going over the free limit means D1 refuses every query on the account until midnight UTC.
  • The rows-per-day numbers are for every D1 database on the account, from Cloudflare’s own analytics.
  • The new indexes went live at 02:37 UTC on 28 October. The rest of the code went live at 05:40 UTC the same morning.
  • The sweeper’s before and after (about 11,600 rows a run down to under 200, on a quiet run) was measured on a full-size copy of the database. The other before and after numbers are from the live one.
  • My agents’ search for one job’s files is the command studio find --run. It has read 2 rows a call since the indexes went live.
  • Every response from the library now also says how many rows it read.