Your database.
The size of R2.
The object holds your working set.
R2 holds your database.
A Durable Object gives you one thread, a page cache, and a durable overlay — a fixed, modest footprint. The database itself is an immutable base of 64 KiB pages in object storage.
The pages this query touched. A RAM cache in front, a durable overlay behind.
The immutable base, paged and checksummed. It keeps going past the edge of this diagram.
Declare it once.
Use it from your Worker.
There is no application to deploy. Declare your tables and indexes, create the database with one command, then read, write and subscribe from the service you already run. The client is typed from your schema, a transaction re-runs itself when a row it read moves, and a live result holds its own lease.
// schema.ts — your tables and indexes, declared once.
import { table, createSchema, string, boolean } from 'r2db';
export const schema = createSchema({
tables: [
table('task')
.columns({ id: string(), title: string(), done: boolean() })
.index('task_by_done', 'done')
.primaryKey('id'),
],
});
A transaction never holds anything open. db.transact runs your callback in your own Worker and re-runs it when a row it read moves under it, so the callback is synchronous and may run more than once: take the clock from tx.now and IDs from tx.id(). Each round trip is one billed request, and reads that do not depend on each other share one. For a write that must survive a lost response, db.write takes an idempotency key: when a response never establishes whether the write landed, the SDK raises WriteOutcomeUnknown carrying that key — retry with it inside the 24 hour window rather than issuing a fresh write.
Start with credits.
Add a card through Stripe, top up a prepaid balance, and watch it in the console. No subscription to navigate.
How billing works →