April 10, 2025·6 min read·engineeringsqlitereplicationconsistency

D1 bookmarks carry consistency across read replicas

D1 read replication enters public beta with a Sessions API. Bookmarks let a client change replicas without reading a database state older than one it has already observed.

A nearby database copy can answer a read faster and still answer the wrong question for the application. After an order is created, a lagging replica can return an order list that omits it. Cloudflare's D1 read-replication public beta, announced on 10 April, addresses that problem with sessions and bookmarks. The application declares which operations belong to one logical history, and D1 carries the history's progress between database copies.

The interesting design choice is to make consistency follow the session when routing changes. A browser can switch networks, or a data center can become unavailable. Pinning a user to one replica would leave correctness dependent on placement decisions outside the application's control. A bookmark instead describes how current the next database copy must be before it can answer.

I like SQLite's local execution model because the application has few intermediaries between a query and its data. D1 deliberately adds a distributed service around SQLite. That can be useful, but the resulting contract needs to explain replica lag as clearly as the local engine explains transactions. The Sessions API makes an otherwise hidden dependency explicit in application code.

A session defines the history that must remain ordered

The launch's implementation account describes asynchronous replication. Different replicas can have applied different amounts of the primary's committed history. A request that reaches one replica and then another can therefore appear to move backward, even if each individual response comes from committed data.

Sequential consistency within a D1 session prevents that regression. Operations issued through the session retain their order, including a read following a write. That is enough to express the order-creation example without requiring every client everywhere to observe every commit immediately. The guarantee concerns the declared logical session, and the application must preserve that session across requests when those requests depend on one another.

Consider a product editor that saves a title and then reloads a preview. The preview needs to include the saved title. A separate visitor who has not observed that save may have a different freshness requirement. Treating both requests as equivalent reads would omit information the service needs. The editor's session provides the relevant ordering dependency.

The application therefore chooses the scope of a logical session. It can correspond to one browser's activity or a mobile application's request sequence. That scope has meaning beyond a single HTTP handler. A fresh Worker invocation needs a way to continue the relevant database history even though it does not retain the previous invocation's in-memory session object.

Bookmarks express a minimum acceptable database state

D1 identifies progress through the write history with bookmarks. A session retains the newest bookmark it observes and sends it with subsequent queries. The selected database copy must be at least that current before serving a dependent read. A replica that is behind waits for the required progress.

That rule is a lower bound on freshness. It does not require a read to reproduce exactly the state represented by the input bookmark. Other writes may have reached the replica before the query executes, so the returned bookmark can be newer. An application should not interpret the token as a request for a historical snapshot at an exact instant.

This difference matters for user interfaces. Continuing a session protects a user from losing visibility of an earlier operation. It does not freeze the rest of the database while the user browses. A later query can observe other committed changes while still satisfying the user's own history. The useful invariant is monotonic progress through the session's observations.

Bookmarks also avoid exposing placement as application state. A client does not need to remember a replica's network address and insist that every request return there. It carries the required progress instead. The service remains free to route work while preserving the consistency condition associated with that progress.

The first query needs its own freshness policy

The API accepts either a previous bookmark or a constraint when creating a session. first-primary directs the first query to the primary. first-unconstrained allows the first query to use a replica without imposing an initial freshness requirement. After the session observes a bookmark, later operations use the progress it has established.

Those choices answer different questions. A public catalog page may accept the state available at a nearby replica when a visitor first arrives. An administrative page may need its first read to begin at the primary. Both can maintain ordered observations afterward. The initial policy determines where the session starts, while bookmarks determine how it continues.

The following Worker fragment follows the launch API and assumes an existing Customers table. It accepts the previous bookmark through a request header and returns the updated bookmark with the result. The client must send that returned value on the next dependent request.

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const bookmark = request.headers.get("x-d1-bookmark")
      ?? "first-unconstrained";
    const session = env.DB.withSession(bookmark);
    const result = await session
      .prepare("SELECT CompanyName FROM Customers")
      .all();
    const response = Response.json(result.results);
    response.headers.set("x-d1-bookmark", session.getBookmark() ?? "");
    return response;
  }
};

The header is an application transport choice in the published example. The consistency mechanism depends on carrying the bookmark, not on that particular header name. A handler that creates a fresh unconstrained session on every request discards the relationship between those requests. Each handler can work correctly in isolation while the overall user workflow loses the property it needs.

Reads can move while writes retain a primary

Read replication does not create several independent write authorities. Cloudflare's implementation routes writes to the primary, including writes that first reach a read replica. A replica can answer an eligible read once it has satisfied the bookmark requirement. The application uses the session interface for both kinds of operation.

This means read scaling and write scaling remain separate questions. Additional read copies can take query work away from the primary. They do not imply that arbitrary writes execute concurrently at every region. A write-heavy application cannot infer a new distributed-write architecture from the availability of regional reads.

The waiting rule also explains why geographical proximity alone cannot determine latency. A nearby replica that lacks a required commit must catch up before it can answer a dependent read. That delay is part of preserving the requested history. Removing the wait would change the answer's semantics, even if it made the individual request finish sooner.

For an application, the distinction between a read that depends on a recent write and an independent read can therefore affect the usefulness of replication. Browsing a catalog and checking the result of a just-completed edit may submit similar SQL. Their consistency requirements differ because of the user's preceding operations. Sessions let the application express that difference without selecting a physical replica itself.

Replica management stays with the service

Session scope also affects how an application describes an operation that spans several requests. An edit followed by a redirect creates an ordering dependency across that redirect. Returning a bookmark only in an intermediate response is insufficient if the next request never receives it. The application needs a transport arrangement that survives its own navigation pattern.

A background refresh has the same issue when its result replaces content the user has already seen. Starting that refresh without the relevant bookmark asks a different consistency question. The request may be independent at the HTTP layer while still belonging to the user's logical database session. Defining that relationship is part of application behavior, because the database service cannot infer it from the text of a SELECT statement.

The beta automatically creates regional copies and routes session requests. The launch changelog describes one replica in each supported D1 region, with activity following traffic. It states that replication adds no separate replica charge. This removes a placement task from the application while preserving the need to use the Sessions API.

There are two independent prerequisites. The database needs read replication enabled, and the Worker needs session-based access. Deploying the API change can precede enabling replicas because the API also works with a non-replicated database. That separation allows the application to establish its consistency behavior before changing where reads execute.

I prefer this explicit session requirement to a promise that geographical routing somehow makes lag irrelevant. The token gives a concrete meaning to continuing a user's database history. It also defines where application responsibility begins. Queries issued outside the session do not automatically acquire ordering relationships with queries inside it.

The practical boundary is a dependent request sequence. A client saves a change, receives the resulting bookmark, and supplies it when requesting the next view. D1 can then use any suitable copy that has reached that progress. The same bookmark remains useful when the next request reaches a different region, which is precisely where relying on replica affinity would have been insufficient.