Quickback Docs

Read replication

Serve reads from D1's regional replicas with the Sessions API — one config flag, one response header, sequential consistency per client.

Cloudflare D1 can keep read-only copies of a database in every region (ENAM, WNAM, WEUR, EEUR, APAC, OC) at no extra cost. Replicas only serve queries made through the D1 Sessions API; plain binding queries always go to the primary. readReplication makes the generated API use that session for every request-scoped feature read.

// quickback.config.ts
database: defineDatabase("cloudflare-d1", {
  readReplication: true,
}),

Then enable replication on the features database itself — Settings → Enable Read Replication in the D1 dashboard, or PUT the database with { "read_replication": { "mode": "auto" } } through the REST API. The compiler never flips that switch. The generated code is correct either way: without replicas every session runs on the primary, so turning the flag on first and replication on later is safe.

D1 only. Postgres providers reject the option; Neon's read replicas are a connection-string concern.

What the generated API does

Each request's db handle runs on a D1 session. The session constraint is chosen by method:

RequestSession startsWhy
GET / HEAD carrying the bookmark headerat that bookmarkThe client continues its own session: monotonic reads, read-your-writes
GET / HEAD without the headerfirst-unconstrainedNearest replica — the common case for a fresh page load
Everything else (POST, PATCH, PUT, DELETE, actions, batch)first-primaryWrites go to the primary anyway; starting there keeps a handler's reads after its own write fresh

Every response carries the session's bookmark:

HTTP/1.1 200 OK
x-d1-bookmark: 00000085-0000000a-00004e6e-9d8f2c1b
x-d1-served-by: WEUR;primary=false

A client that sends the bookmark back on its next GET gets sequential consistency across requests: it never reads an older version of the database than the one it last saw, and it sees its own writes. A client that ignores the header gets eventual consistency on reads only — the same thing any replicated database gives an un-sessioned reader — and still gets fresh reads inside every write. Both headers are CORS-exposed so browser code can read them.

x-d1-served-by is for verifying a rollout: it is set only when D1 reports where the session ran (meta.served_by_region / served_by_primary), which is every remote request and never wrangler dev.

Echo the bookmark from your client

let bookmark: string | null = null;

async function api(path: string, init: RequestInit = {}) {
  const headers = new Headers(init.headers);
  if (bookmark) headers.set("x-d1-bookmark", bookmark);
  const res = await fetch(`${API}${path}`, { ...init, headers, credentials: "include" });
  bookmark = res.headers.get("x-d1-bookmark") ?? bookmark;
  return res;
}

One variable per browser tab or app session is enough. Sending a bookmark from a different session is harmless: D1 only guarantees the new session is at least as fresh as the bookmark.

Only a well-formed bookmark (four 8-hex groups, as D1 issues them) continues a session. Any other header value is ignored and the request starts first-unconstrained, so a stale or hand-edited header can never fail a query.

Rename the header

readReplication: { header: "x-bookmark" },

The name must be a valid HTTP header token. It is matched case-insensitively on the way in and emitted lowercase.

What stays on the primary

Only the per-request features db rides the session. These keep their own primary handles, exactly as before:

  • the auth database (Better Auth's adapter and c.get('authDb')) — session and membership rows must never be read from a lagging replica
  • the audit and webhooks databases
  • schedules, queues and email handlers — no request, no client, no bookmark
  • Durable Objects and anything that builds its own drizzle(env.DB)

A read that must be fresh regardless of the client — polling a payment status, an OTP flow — belongs in an action or a non-GET route, which starts on the primary.

Replica lag

Replication is asynchronous and a first-unconstrained read may be arbitrarily old. D1 bounds it per session, not globally: the session waits until a replica has caught up to the bookmark you passed. That is why writes start first-primary and why the bookmark is worth echoing.

With queryTimeoutMs set, the deadline wraps the session as well — a replica that takes too long to catch up to a bookmark answers 503 DB_TIMEOUT like any other slow read.

On this page