Skip to content

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.

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.

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 via storage.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 precision

reserve(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.

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.
}

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 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]

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.

Most apps don’t need multi-region:

  • Single region + Cloudflare edge cache: GET /sync/pull responses 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 intermediate puts 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.