D1 read replicas: withSession() plus a bookmark across requests

Enable read replication, run every query in a session, and pass the last bookmark so a user never reads older data than they wrote. Writes hit the primary.

KV/R2/D1

· Chapter

53

·

2

min read

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 dev has no replicas; served_by_region is 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