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.
Postgres via Hyperdrive scales further but at the cost of a cross-region hop for every write. Global apps eventually want the mutation applied close to the user.
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]Hyperdrive + Postgres
Section titled “Hyperdrive + Postgres”For a Postgres-backed multi-region deploy, use Hyperdrive to give each region low-latency access to a shared Postgres instance:
import { fromPostgres } from "@sh1n4ps/plasma-server/postgres-adapter"
const executor = fromPostgres(env.HYPERDRIVE.connectionString)The change log lives in Postgres. Writes still serialise at the DB layer, but Hyperdrive amortises the cross-region latency for reads and pulls.
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