Cloudflare D1 Free Tier Limits: One Endpoint Can Spend Your Whole Account (2026)

On 6 September 2026, a solo developer's uptime-monitor project logged roughly 11 million D1 rows read in 24 hours and wrote up their own postmortem in the repo's issue tracker. Its public /incidents page ran an unbounded scan of the whole history table on every view. One query shape accounted for 10.49 million of those rows across 167 invocations, which works out to about 63,000 rows per page view. The account-wide free allowance is 5 million a day, so the project spent it, and an unrelated database on the same account (links-db) stopped answering too.

No attacker was involved. That's the point. 167 ordinary page views, on a project nobody had heard of, spent an allowance that Cloudflare had begun enforcing five days earlier.

TL;DR

TL;DR: Cloudflare fails D1 queries outright once a Workers Free account crosses 5 million rows read or 100,000 rows written in a day. The counter covers the whole account and measures rows examined, not returned, so one unbounded public endpoint can take every database on the account offline until midnight UTC. Check your slowest public query first.

What the changelog says, and what it doesn't

Cloudflare's entry is one sentence: "Beginning September 1, 2026, D1 queries on the Workers Free plan will fail when an account exceeds the daily row read or row write limits."

Both surfaces are covered. "Queries via the Workers Binding API and the REST API will return errors." Stored data isn't touched; you just can't query it. The error text names the limit you hit and tells you to upgrade or wait:

What your app gets instead of data
Your account has exceeded D1's free tier daily row read limit.
Upgrade to a paid plan or wait until tomorrow (midnight UTC) to continue.

Worth knowing before you treat 1 September as the day everything changed: Cloudflare announced the same thing once before. A D1 release note dated 13 January 2025 reads "D1 will begin enforcing its free tier limits from the 10th of February 2025", with near-identical wording about queries returning errors until limits reset at 00:00 UTC.

So this is the second announcement of the same enforcement, and Cloudflare has published nothing since 1 September confirming it took effect or walking it back. Don't plan around the date. Plan around the fact that your queries can start failing, which was already true on paper.

The counter measures work, not results

D1 bills rows read, and a row read is a row the database had to look at. Cloudflare's pricing page spells it out: "if you have a table with 5000 rows and run a SELECT * FROM table as a full table scan, this would count as 5,000 rows read."

That's the whole trap. A page that displays ten results can charge you five thousand rows, and it gets worse every week as the table grows. The same page that cost 500 rows at launch costs 63,000 a few months in, and nobody redeployed anything.

An index on the column you filter by is the fix Cloudflare names: with an index on created_at, a query filtering on created_at "would only need to read a subset of the table."

The cost of a query is a function of your data, not your code. An endpoint that was cheap when you shipped it gets more expensive every day you keep using the product. This is the one performance bug that gets worse while you do nothing.

Account-wide is the part that hurts

The free plan's 5 million rows read per day and 100,000 rows written per day are account limits, not database limits. Cloudflare's docs put it plainly: "When your account hits the daily read and/or write limits, you will not be able to run queries against D1." Every database under the account draws from one pot.

So the blast radius of one careless endpoint is not that endpoint. It's everything you have on that Cloudflare account: the side project, the client demo, the thing you show in interviews. The 6 September incident is exactly this shape. One project's status page took down a URL shortener that shared nothing with it but an account.

The arithmetic is unkind

Workers Free allows 100,000 requests a day. D1 Free allows 5 million rows read a day.

Line those up against an endpoint costing 63,000 rows a view and the read budget runs out after about 80 requests, against a plan that will happily serve 100,000. Your hosting can absorb roughly 1,250 times more traffic than your database can.

Two honest caveats on that 80. It's specific to one app's unbounded scan over a history table that had been growing for months, so a smaller or newer table is far cheaper per call. And it assumes the rest of the account is idle, which in the documented case it wasn't: the 11 million total included other queries.

What survives the caveats is the shape. Nobody has to attack you for this. 167 page views did it, and a moderately enthusiastic crawler produces more than that in a morning.

Writes are the cheaper target, and it's worth doing this arithmetic too. The daily write allowance is 100,000 rows, fifty times smaller than the read allowance. Any public endpoint that inserts a row per call (a waitlist signup, a contact form, an anonymous analytics ping) hands a visitor a counter they can increment one request at a time, and 100,000 requests is well inside a single Worker's free daily budget.

Find your expensive endpoint in five minutes

Work through this in order. The first step usually answers it.

1

List every route that queries D1 without checking auth

Search your Worker for the D1 binding and read each call site. Any handler that runs a query before it checks a session or a token is a handler a stranger can run as often as they like.

2

Find the queries with no WHERE bound and no LIMIT

SELECT * FROM table with no filter is the obvious one. The subtle one is a filter on a column with no index, or a time window that starts at zero. In the September incident the query looked bounded: it filtered by target_id. The time bound was sinceMs = 0, so it read the entire history every time.

3

Check what those columns are indexed on

Run PRAGMA index_list('your_table') against the database. If the column in your WHERE clause isn't in an index, every query on it is a full table scan, and the scan grows with the table.

4

Read your own analytics, not your intuition

D1's dashboard shows rows read per query shape. The query burning your quota is usually not the one you'd guess: in the incident, a single normalized query accounted for 76.6% of the database's runtime.

What to do about it

Four changes, roughly in order of how much they buy you:

  • Bound the query. A time window and a LIMIT turn an unbounded scan into a fixed cost. The incident's fix was bounding per-target scans to the past year.
  • Index the filter column. This is the difference between reading a table and reading a slice of one.
  • Cache the public response. A public page that changes every few minutes doesn't need to hit the database on every view. Cloudflare's own cache in front of the Worker costs you nothing here.
  • Put auth in front of anything that isn't genuinely public. An endpoint nobody can reach anonymously can't be spent anonymously.

Collapse round trips while you're in there. The September fix also merged four separate queries behind one detail page into a single one. Four cheap queries per view still multiply by every view you get.

The general shape, past this one product

None of this is a discovery. Developers have been writing it into their own issue trackers: on 25 September 2026, one project opened a ticket titled "Quota exhaustion and denial-of-wallet: confirm the Workers plan and set spend guardrails", noting that on Workers Free "anyone with a script can take the whole service offline for the rest of the UTC day", that "production, beta and previews share the account quota", and that "Cloudflare's DDoS protection won't stop low-and-slow traffic that looks legitimate". What's missing isn't the insight. It's anyone saying it outside a repo that four people will read.

D1 is one instance of a pattern. Any free tier that stops serving when you cross a line is an availability budget, and anything that can reach your app gets to spend it. An expensive endpoint stops being a performance footnote the moment the platform answers a quota overrun with an error instead of a graph.

So the question to ask about a free tier isn't what it costs you. It's who else gets to spend it, and how much each of them can spend per request.

What are the Cloudflare D1 free tier limits?

On the Workers Free plan, D1 allows 5 million rows read per day, 100,000 rows written per day, and 5 GB of storage in total. The daily counters reset at midnight UTC. These are account limits rather than per-database limits: Cloudflare's docs say that when your account hits the daily read or write limits, you will not be able to run queries against D1. Every database on the account draws from the same allowance.

What happens when you exceed the D1 free tier daily limit?

The query fails. Cloudflare returns an error through both the Workers Binding API and the REST API, with a message along the lines of: Your account has exceeded D1's free tier daily row read limit. Upgrade to a paid plan or wait until tomorrow (midnight UTC) to continue. Stored data is not affected, but nothing degrades gracefully either, and the app stays broken until the counter resets at midnight UTC or you upgrade. Cloudflare announced this enforcement for 2025-02-10 and again for 2026-09-01, so treat it as live rather than upcoming.

Does a D1 row read mean a row my query returned?

No, and this is where the arithmetic surprises people. Cloudflare counts rows examined. Their own example: a full table scan with SELECT * on a table of 5,000 rows counts as 5,000 rows read, even if you then show ten of them. An index on the column you filter by cuts the count to the subset the index points at.

Can someone deliberately exhaust my D1 quota?

If you have a public endpoint that runs an unbounded or unindexed query, then yes, and nothing about it requires intent: a crawler works as well as an attacker. Cloudflare's CDN does not cache HTML or JSON by default, and Bot Fight Mode matches known bots rather than a plain script, so neither is a defence you already have. The controls are on your side: bound the query, index the filter column, cache the response yourself, and put authentication in front of anything that does not need to be public.

Is the paid plan affected the same way?

No. Workers Paid includes 25 billion rows read per month and bills overage at $0.001 per million rows read, so the same traffic pattern turns into a bill rather than an outage. That is a different problem, not a smaller one, and the same fixes apply.

Find the endpoints anyone can reach

CheckYourVibe scans your deployed app for endpoints that answer without authentication, which is where this class of problem starts.

How-To Guides

Cloudflare D1 Free Tier Limits: One Endpoint Can Spend Your Whole Account (2026)