Why Durable Objects Charge for Rows, Not Response Bytes
Why Durable Objects bill rows instead of response bytes, with a reproducible index-cost example that accounts for CrabTalk and Cloudflare plan allowances.
CrabTalk Team
Imagine a chat room implemented as a Durable Object. It stores messages in SQLite, answers requests for history, and broadcasts new messages to connected users.
One request might look up a message by its primary key. Another might scan the entire history to calculate an unread count. Both can return a few bytes.
That is the starting point for understanding Cloudflare’s pricing: the reply tells you what the application produced, but says little about what it took to produce it.
For SQLite-backed Durable Objects, Cloudflare meters rows read and written, alongside requests, duration, and stored data. Response bytes are not a separate storage billing unit. The older key-value storage backend has different pricing; this article concerns the SQLite backend. Durable Objects pricing
A small answer can require a large scan
Consider this illustrative query inside our room’s Durable Object:
SELECT COUNT(*) AS unread
FROM messages
WHERE sequence > ? AND sender_id != ?;It returns one row containing one number. Depending on the schema, indexes, and data, SQLite may have to inspect many messages to calculate that number.
Now compare it with:
SELECT body
FROM messages
WHERE id = ?;Assume id is the primary key. This query finds a specific record. Its response might be much larger than the unread count, even though finding it requires far less searching.
Billing by returned rows would treat both as one row. Billing by response bytes could make the straightforward lookup look more expensive. Neither captures the difference in their access patterns.
Durable Objects expose rowsRead and rowsWritten on the SQL cursor. These counters report work performed by the query, and their final values are used for billing. They can increase as the cursor is consumed, so inspect them after reading the results. SQL cursor documentation
The distinction matters even when a query returns nothing. An unsuccessful search can still involve substantial work.
Why storage operations have their own meter
A Durable Object brings application code and persistent storage together. SQLite executes as a library in the same thread as the object’s code. Reading a query result inside the object does not require a round trip to a separate database server. Cloudflare’s architecture explanation
That makes storage fast to access. It does not make storage activity disappear.
A request can manipulate only in-memory state, read a few records, or rewrite a large collection. Request count alone cannot distinguish those cases. Duration measures how long the object remains billable, rather than counting its database operations. Cloudflare charges duration while an object is active or unable to hibernate; that is wall-clock time, not a standalone measurement of SQL CPU usage. Compute billing
The meters therefore describe different dimensions: invocations, active lifetime, database activity, and retained data.
Cloudflare could have bundled database work into a higher duration price. Separate row charges are a pricing choice, not a technical necessity. My interpretation is that they let storage-intensive workloads contribute more without putting all of that usage into a single time-based rate. The public rate card establishes the meters; it does not establish how closely each rate matches Cloudflare’s internal costs.
Why returning data has no separate fee
There are two boundaries to distinguish: SQLite returning records to your code, and your application delivering a response to a client.
The first happens locally within the Durable Object. The second involves the network. Cloudflare’s Workers pricing states that there are no additional egress or bandwidth charges for the covered usage. Workers pricing
For our chat room, that means retrieving history and delivering history are different activities. Reading records incurs storage usage. Sending the resulting bytes does not add a per-GB response fee.
It also creates a useful consequence for broadcasts. Suppose the application persists a new message once, then uses the message already in memory to send it to 100 connected clients. Sending those copies does not itself require 100 database reads. Cloudflare also specifies that outgoing WebSocket messages have no request charge, although running the object still has duration implications. WebSocket billing
That makes fan-out easier to reason about: the number of recipients does not automatically multiply storage operations.
This is a consequence of the model, not proof of Cloudflare’s motive. Networking still consumes resources. A provider can recover costs through other charges instead of exposing every resource as a separate invoice item.
Rows are a useful approximation
Why count rows rather than every disk page, CPU instruction, or byte processed?
My reading is that rows provide an application-level unit developers can connect to their queries. You can investigate which query scans too much, which update touches too many records, and which index changes the access pattern.
But a row is not a fixed amount of physical work. A short record and a large document have different memory and processing costs. A cached read and a read requiring disk access need not cost the infrastructure the same amount.
The row meter simplifies those differences. It should be understood as a billing abstraction, not a precise accounting of hardware consumption. Larger records still occupy more stored bytes and can make application processing more expensive, even when they do not increase the number of logical records accessed.
That is also why packing everything into one giant JSON record is a poor default optimization. It can reduce the number of records touched while forcing the application to load, parse, and rewrite unrelated state. A lower row count is only one part of the result.
Indexes exchange writes for reads
An index can help the unread-count query skip irrelevant history. Maintaining that index also creates additional written rows. Cloudflare documents both effects. Indexes in SQLite
The tradeoff deserves arithmetic. Beyond included usage, the published rates are $0.001 per million rows read and $1 per million rows written. At those marginal operation rates, one additional written row costs as much as 1,000 read rows. Storage pricing
An index can therefore improve latency without reducing the bill. Whether it saves money depends on how frequently records change and how many reads it avoids over their lifetime. Storage size and duration also matter.
For the room’s unread count, the practical comparison is between scanning history, using a suitable index, and maintaining a summary as messages arrive. Each moves work to a different point in the application. Measure the actual query counters before deciding.
A reproducible index-cost example
Consider the research library in our CrabTalk and Cloudflare comparison. One agent adds findings; another looks up findings for a project. Both need the same persistent records. If an existing MCP service already supplies those tools and the required access, both agents can use it. The calculation below concerns hosting the library yourself.
Assume 100,000 inserts per month. Each insert writes one table row, and maintaining an index adds one written row. Assume a lookup reads 10,000 rows without the index and 10 with it. These are illustrative counter values, not measured query results; schema, selectivity, and indexes determine the actual counters on each platform. Index creation, updates, deletes, and reads performed during inserts are outside this model.
At $0.001 per million rows read and $1 per million rows written, the unrounded row-rate calculation is:
scan = queries × 10,000 / 1,000,000 × $0.001
+ 100,000 / 1,000,000 × $1
index = queries × 10 / 1,000,000 × $0.001
+ 200,000 / 1,000,000 × $1| Queries per month | Scan, before allowances | Index, before allowances |
|---|---|---|
| 1,000 | $0.11000 | $0.20001 |
| 1,000,000 | $10.10000 | $0.21000 |
| 10,000,000 | $100.10000 | $0.30000 |
The extra index writes cost $0.10 at these marginal rates. They pay for themselves after about 10,010 lookups under these assumptions. At 1,000 lookups, fewer reads do not offset the added writes.
Apply the remaining allowances
Rates and allowances were checked November 5, 2026. CrabTalk’s public execution rates and plans specify the same row rates used above, with 20 billion reads and 50 million writes included on Go. Cloudflare’s Workers Paid SQLite allowance includes 25 billion reads and 50 million writes per month. These are account allowances, shared with other workloads.
With the full row allowances still available:
| Plan | 1 million queries: scan / index | 10 million queries: scan / index |
|---|---|---|
| CrabTalk Go | $0 / $0 | $80 / $0 |
| Workers Paid | $0 / $0 | $75 / $0 |
At one million queries, both approaches fit inside both plans’ row allowances. At ten million, the scan reads 100 billion rows, leaving 80 billion billable on Go and 75 billion on Workers Paid. The indexed version reads 100 million rows and writes 200,000, within both allowances. These examples have whole-million read overages and no write overages; Cloudflare’s billing-unit rounding does not change those results.
Run this JavaScript with Node or Bun to reproduce both tables. Change the remaining allowances if other services have consumed part of them.
const inserts = 100_000;
const million = 1_000_000;
const plans = [
{ name: "Before allowances", read: 0, written: 0 },
{ name: "CrabTalk Go", read: 20_000_000_000, written: 50_000_000 },
{ name: "Workers Paid", read: 25_000_000_000, written: 50_000_000 },
];
for (const queries of [1_000, 1_000_000, 10_000_000]) {
for (const plan of plans) {
const costs = [
{ name: "scan", read: queries * 10_000, written: inserts },
{ name: "index", read: queries * 10, written: inserts * 2 },
].map((usage) => ({
approach: usage.name,
usd: (
Math.max(0, usage.read - plan.read) / million * 0.001 +
Math.max(0, usage.written - plan.written) / million
).toFixed(5),
}));
console.log(queries, plan.name, costs);
}
}This is database-operation arithmetic, not a benchmark or a complete hosting bill. Add the subscription, requests or runs, Worker CPU, object duration or CrabTalk CPU and memory, retained storage, and any upstream API or model charges. Go and Workers Paid each start at $5 per month, with different included resources. A Workers Free workload has daily limits instead of these paid-plan allowances. CrabTalk plans, Workers pricing, and Durable Objects pricing define those separate charges and limits.
For real inputs, consume the SQL cursor before reading Cloudflare’s rowsRead and rowsWritten. On CrabTalk, inspect the read and written fields returned by berm_lang::sql, then use crab bench and production call usage to estimate and check the rest of the workload. Identical SQL does not establish identical measured usage across hosts.
Durable Objects’ pricing makes that work visible. A tiny reply can hide an expensive access pattern; a message delivered to many people can reuse one stored result. The useful question is how much persistent state the application touches to serve its users.