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: numbered migrations, prepare().bind(), batch() for atomicity · KV is a cache with sixty-second edges, not a database · Hyperdrive pools Postgres connections so Workers skip the handshake