
Cloudflare loosened one Workers limit and tightened another
In the space of four days Cloudflare made it easier to deploy a Worker and harder to run a free database behind it. On September 4 they dropped the compressed bundle check that rejected deploys over 3 MB on the Free plan and 10 MB on Paid. On September 1, D1 began returning errors the moment a free account passes 5 million rows read or 100,000 rows written in a day. Both land on the same kind of project: the small app someone ships without a budget.
The size ceiling was measuring the wrong number
Wrangler bundles your code and gzips it before upload. Cloudflare used to check the gzipped figure and refuse anything over 3 MB on Free, 10 MB on Paid. That check is gone. What counts now is the uncompressed bundle against a 64 MiB ceiling, and the ceiling is identical whether you pay or not.
The gap is wider than the numbers imply, because compression ratios vary with what you ship. Cloudflare's own example in the changelog shows a bundle at 259.61 KiB uncompressed and 47.23 KiB gzipped, about five and a half to one. A dependency full of repetitive JavaScript compresses like that. A WASM module or an embedded font does not, so under the old rule two projects of the same real size could get different answers from the same deploy command, and you only found out after the upload. Checking uncompressed size is the less clever measure and the more predictable one.
If you want the number before you push, run wrangler deploy --outdir bundled/ --dry-run. The Total Upload line is what Cloudflare now counts. The gzip figure beside it is trivia.
D1's free tier stopped being a suggestion
Since September 1, exceeding D1's daily row limits on the Workers Free plan fails your queries rather than just showing up in a dashboard. Both the Workers binding API and the REST API return an error telling you to upgrade or wait for midnight UTC. Cloudflare is explicit that stored data is untouched, so this is a pause rather than a loss, which is the difference between an annoying morning and a bad one.
Five million rows read per day reads as generous until you notice D1 counts rows scanned, not rows returned. Cloudflare's pricing page spells it out: SELECT * on a 5,000-row table is 5,000 reads whether you keep one row or all of them, and filtering on a column with no index still scans the table to decide what to drop. An app doing one unindexed lookup per page view spends the daily allowance in a thousand views. The fix is an index on the column you filter by, and it gets skipped constantly, because on a table that small nothing feels slow enough to investigate.
The paid plan includes 25 billion rows read a month and charges $0.001 per million after that, so the money was never the point of the free limits. Enforcement is about people leaving a full table scan running in a loop. Fair enough, though I'd rather Cloudflare had warned by email a week before the queries started dying.
What this changes for how we build
Produlis ships client sites on Workers, so both changes touch our own deploys. The size one we welcomed and mostly ignored, since a static Astro build rarely comes near 64 MiB. The D1 one changed a habit: we now check the rows_read value in the meta object that every D1 query returns, during development, before anything goes live. A query that reads 4,000 rows to render a page is a bug regardless of which plan pays for it.
The rule we settled on is boring. Index every column you filter or sort by, before launch, not after the first slow week. On a booking page that queries availability by date, the index on the date column is the difference between reading a handful of rows and reading the table. Cloudflare's enforcement did not create that problem. It just made it visible on day one instead of month six.
