Multi-Region
plasma’s default deploy is single-region: one Worker, one D1, one
change log with a monotonically-increasing row_version allocated
by the DB. This works up to the throughput a single D1 can push.
Beyond that, multi-region.
Why single-region breaks
Section titled “Why single-region breaks”D1 (like most SQLite-backed stores) serialises writes. Every mutation
hits the same DB. Above a few hundred writes / second sustained,
you start seeing SQLITE_BUSY under contention and the change log’s
INSERT becomes the bottleneck.
The two primitives
Section titled “The two primitives”plasma v1.0 ships the primitives you’d compose into a multi-region deploy:
SequencerDO— a per-region Durable Object that mints monotonic version IDs viastorage.transaction. Each region owns its own DO instance keyed by region ID.- The causal cookie — the pull cookie already carries per-region
cursors (
{ "0": "123", "1": "456" }) so clients can pull from any region without missing writes.
SequencerDO — the region-local id source
Section titled “SequencerDO — the region-local id source”// worker.ts (the DO's own script)import { SequencerDO, MemorySequencerStorage } from "@sh1n4ps/plasma-server"
export class Sequencer extends SequencerDO { constructor(state: DurableObjectState, env: Env) { super({ storage: state.storage }) }}
// Callers reserve version IDs:const stub = env.SEQUENCER.get(env.SEQUENCER.idFromName(`region-${regionID}`))const res = await stub.reserve(1)// { start: "12345", end: "12345" } — strings to preserve bigint precisionreserve(count) allocates a contiguous [start, end] range so a
batch of writes gets a run of consecutive IDs without a round-trip
per write.
The IDs are stringified bigints — the DO can mint past 2^53
without JS number precision loss, and the JSON boundary between the
DO and its caller preserves the full value.
Per-region assignment
Section titled “Per-region assignment”You’d assign the region ID from the request’s Cloudflare metadata:
async function fetchWithRegion(req: Request, env: Env) { const cf = req.cf as { region?: string } | undefined const regionID = mapCfRegionToPlasmaRegion(cf?.region ?? "default") // ... reserve version IDs via env.SEQUENCER for that regionID.}Change log with origin_region
Section titled “Change log with origin_region”The change log’s origin_region column identifies which region
produced each write:
_plasma_changes: row_version origin_region table row_id op value 123 0 todos t1 put {...} 456 1 todos t2 put {...}Region 0 (say US) and Region 1 (say EU) both allocate row_versions
from their own SequencerDO. They coordinate by writing to their
regional DBs and replicating changes cross-region — the pull loop
serves clients from whichever region is closest.
The causal cookie
Section titled “The causal cookie”The pull cookie is a base64-encoded JSON object keyed by region:
c1:eyIwIjoiMTIzIiwiMSI6IjQ1NiJ9= c1: + base64({ "0": "123", "1": "456" })Each entry is “the highest row_version I’ve seen from this region.”
On pull, the server (in whatever region the client hit) returns
every change with row_version > cookie[origin_region] for every
region.
advanceCookieFromChanges(cookie, rows) folds the incoming changes
back into the cookie shape.
pullPredicate(cookie) produces the SQL WHERE clause that selects
“any row_version this cookie hasn’t seen”:
const { text, params } = pullPredicate(cookie)// text: "(_plasma_changes.origin_region = ? AND _plasma_changes.row_version > ?)// OR (_plasma_changes.origin_region = ? AND _plasma_changes.row_version > ?)"// params: [0, 123, 1, 456]The simplification path
Section titled “The simplification path”Most apps don’t need multi-region:
- Single region + Cloudflare edge cache:
GET /sync/pullresponses are cached at the edge in the Cache API layer, so a fresh client’s initial hydration doesn’t hit D1 twice. - Single region +
compactChangeLog: run this from a scheduled Worker to fold intermediateputs on the same(table, row_id)down to the latest. Keeps the change log small and pulls fast.
Reach for multi-region when you have sustained write bandwidth above what a single D1 handles, or when regulatory / latency requirements force data locality.
What to read next
Section titled “What to read next”- Concepts / Change Log and Cookies — the primitives multi-region composes on top of
- Deployment — where to install
SequencerDOinwrangler.jsonc