The answer. D1 is one primary database in one region; read replication adds read-only copies in other regions so reads from far away stop crossing an ocean. It is opt-in per database and it changes one thing in code: queries run through a session. env.DB.withSession("first-unconstrained") starts anywhere (fastest first read), "first-primary" starts at the primary (use it when the session begins with a write), and passing a bookmark string starts from a database version at least as new as that bookmark. Return session.getBookmark() to the client in a header or cookie, and pass it back on the next request: the user then never observes their own write disappearing on a replica that has not caught up. Writes are routed to the primary regardless of what you pass.
The pattern.
export default {
async fetch(request, env) {
const bookmark = request.headers.get("x-d1-bookmark") ?? "first-unconstrained";
const session = env.DB.withSession(bookmark);
const { results, meta } = await session.prepare("select * from products order by id desc limit 20").all();
const res = Response.json(results);
res.headers.set("x-d1-bookmark", session.getBookmark() ?? "");
res.headers.set("x-served-by", `${meta.served_by_region}${meta.served_by_primary ? " (primary)" : ""}`);
return res;
},
} satisfies ExportedHandler<Env>;
Watch out.
- Sequential consistency holds inside one session. Two sessions without a shared bookmark can disagree for a moment; that is the price of replicas.
- Local
wrangler devhas no replicas;served_by_regionis undefined there, so test the header plumbing, not the latency. - Read-heavy is the use case. A write-heavy table gains nothing and pays replication lag.
Related: d1-migrations-prepared-statements-and-batch · kv-is-a-cache-with-sixty-second-edges · hyperdrive-pools-postgres-connections-for-workers