Sharding
Default __root__ DO, opt-in .shardBy(), and .global() escape hatch.
Last updated:
Lunora scales out, but starts on a single shard. New apps get one Durable
Object, addressable as __root__, that holds every table without an explicit
tier modifier. This is the Zeroback shape, and the right answer for the first
80% of an app's life.
When to shard
The per-DO ceiling is 10 GB of SQLite and roughly 1 000 sustained req/s.
When the __root__ DO crosses 1 GB, the runtime emits a console.warn once
per DO lifetime so you can plan the migration before you hit the wall. The
threshold (1 073 741 824 bytes) is 10% of the per-DO ceiling, far enough
below it that a .shardBy() migration has runway to land.
[@lunora/do] __root__ Durable Object SQLite size is 1075000000 bytes
(>= 1 GiB, 10% of the 10 GiB per-DO ceiling). Plan a `.shardBy()` migration
before you hit the wall. See https://lunora.sh/docs/concepts/sharding for
guidance.The warning fires from ShardDO after every RPC that touches storage and
deduplicates itself with a static flag, so it shows up at most once per DO
instance. Non-__root__ DOs are never affected, even when they exceed
1 GB on their own.
Sharding is a single edit:
messages: defineTable({ channelId: v.id("channels"), text: v.string() }).shardBy("channelId");After codegen, every call to ctx.db.messages.* routes by channelId. A
chat with 5 000 active channels now spreads across 5 000 DOs, each with its
own SQLite, CPU budget, and hibernation timer.
What stays in the root DO
Anything without .shardBy() or .global() stays in __root__. That keeps
the common case (app config, feature flags, small per-app state) cheap and
strongly consistent, with no routing overhead.
Going global
For cross-tenant data (identities, billing, account-wide audit logs) use
.global(). The table lives in Cloudflare D1 instead of a DO. Reads can
hit a regional replica; writes go through the primary. Pass the D1Session
bookmark via the x-d1-bookmark header for read-your-writes consistency.
users: defineTable({ email: v.string(), name: v.string() }).global().index("by_email", ["email"], { unique: true });Cross-shard reads
ctx.db.query("messages").collect() against a .shardBy() table that
doesn't pin a shard is a fan-out: codegen routes it through the Query
Coordinator Worker, which dispatches to every shard and merges the results.
Avoid this in hot paths.