Skip to content
Cloudflare Docs

Use read replication

D1 read replication can lower latency for read queries and scale read throughput by adding read-only database copies, called read replicas, across regions globally closer to clients.

Your application can use read replicas with D1 Sessions API. A Session encapsulates all the queries from one logical session for your application. For example, a Session may correspond to all queries coming from a particular web browser session. All queries within a Session read from a database instance which is as up-to-date as your query needs it to be. Sessions API ensures sequential consistency for all queries in a Session.

To use read replication with the following Worker code and Sessions API:

  1. Enable read replication on a D1 database:
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{"read_replication": {"mode": "auto"}}"
  1. Deploy the following Worker using the Deploy to Workers button.
6 collapsed lines
import { D1Database, D1DatabaseSession } from "@cloudflare/workers-types";
export type Env = {
DB01: D1Database;
};
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
// A. Create the Session.
// When we create a D1 Session, we can continue where we left off from a previous
// Session if we have that Session's last bookmark or use a constraint.
const bookmark = request.headers.get('x-d1-bookmark') ?? 'first-unconstrained';
const session = env.DB01.withSession(bookmark);
try {
// Use this Session for all our Workers' routes.
const response = await withTablesInitialized(session, async () => await handleRequest(request, session));
// B. Return the bookmark so we can continue the Session in another request.
response.headers.set('x-d1-bookmark', session.getBookmark() ?? "");
return response;
} catch (e) {
console.error({ message: "Failed to handle request", error: String(e), errorProps: e, url, bookmark });
return Response.json(
{ error: String(e), errorDetails: e },
{ status: 500 }
);
}
},
97 collapsed lines
} satisfies ExportedHandler<Env>;
type Order = {
orderId: string,
customerId: string,
quantity: number,
}
async function handleRequest(request: Request, session: D1DatabaseSession) {
const { pathname } = new URL(request.url);
const tsStart = Date.now();
if (request.method === "GET" && pathname === '/api/orders') {
// C. Session read query.
const resp = await session.prepare('SELECT * FROM Orders').all();
return Response.json(buildResponse(session, resp, tsStart));
} else if (request.method === "POST" && pathname === '/api/orders') {
const order = await request.json<Order>();
// D. Session write query.
// Since this is a write query, D1 will transparently forward the query.
await session
.prepare('INSERT INTO Orders VALUES (?, ?, ?)')
.bind(order.customerId, order.orderId, order.quantity)
.run();
// E. Session read-after-write query.
// In order for the application to be correct, this SELECT
// statement must see the results of the INSERT statement above.
const resp = await session
.prepare('SELECT * FROM Orders')
.all();
return Response.json(buildResponse(session, resp, tsStart));
} else if (request.method === "POST" && pathname === '/api/reset') {
const resp = await resetTables(session);
return Response.json(buildResponse(session, resp, tsStart));
}
return new Response('Not found', { status: 404 })
}
function buildResponse(session: D1DatabaseSession, res: D1Result, tsStart: number) {
return {
d1Latency: Date.now() - tsStart,
results: res.results,
servedByRegion: res.meta.served_by_region ?? "",
servedByPrimary: res.meta.served_by_primary ?? "",
// Add the session bookmark inside the response body too.
sessionBookmark: session.getBookmark(),
};
}
/**
* This is mostly for DEMO purposes to avoid having to do separate schema migrations step.
* This will check if the error is because our main table is missing, and if it is create the table
* and rerun the handler.
*/
async function withTablesInitialized(session: D1DatabaseSession, handler: () => Promise<Response>) {
try {
return await handler();
} catch (e) {
if (String(e).includes("no such table: Orders: SQLITE_ERROR")) {
await initTables(session);
return await handler();
}
throw e;
}
}
async function initTables(session: D1DatabaseSession) {
return await session
.prepare(`CREATE TABLE IF NOT EXISTS Orders(
customerId TEXT NOT NULL,
orderId TEXT NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (customerId, orderId)
)`)
.all();
}
async function resetTables(session: D1DatabaseSession) {
return await session
.prepare(`DROP TABLE IF EXISTS Orders; CREATE TABLE IF NOT EXISTS Orders(
customerId TEXT NOT NULL,
orderId TEXT NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (customerId, orderId)
)`)
.all();
}

Primary database instance vs read replicas

D1 read replication concept

When using D1 without read replication, D1 routes all queries (both read and write) to a specific database instance in one location in the world, known as the primary database instance . D1 request latency is dependent on the physical proximity of a user to the primary database instance. Users located further away from the primary database instance experience longer request latency due to network round-trip time.

When using read replication, D1 creates multiple asynchronously replicated copies of the primary database instance, which only serve read requests, called read replicas . D1 creates the read replicas in multiple regions throughout the world across the Cloudflare network.

Even though a user may be located far away from the primary database instance, they could be close to a read replica. When D1 routes read requests to the read replica instead of the primary database instance, the user enjoys faster responses for their read queries.

D1 asynchronously replicates changes from the primary database instance to all read replicas. This means that at any given time, a read replica may be arbitrarily out of date. The time it takes for the latest committed data in the primary database instance to be replicated to the read replica is known as the replica lag . Replica lag and non-deterministic routing to individual replicas can lead to application data consistency issues. The D1 Sessions API solves this by ensuring sequential consistency. For more information, refer to replica lag and consistency model. All write queries are still forwarded to the primary database instance. Read replication only improves the response time for read query requests.

Type of database instanceDescriptionHow it handles write queriesHow it handles read queries
Primary database instanceThe database instance containing the “original” copy of the databaseCan serve write queriesCan serve read queries
Read replica database instanceA database instance containing a copy of the original database which asynchronously receives updates from the primary database instanceForwards any write queries to the primary database instanceCan serve read queries using its own copy of the database

Sessions API examples

By using Sessions API for read replication, all of your queries from a single Session read from a version of the database which ensures sequential consistency. This ensures that the version of the database you are reading is logically consistent even if the queries are handled by different read replicas.

D1 read replication achieves this by attaching a bookmark to each query within a Session. For more information, refer to Bookmarks.

Enable read replication

With the REST API, set read_replication.mode: auto to enable read replication on a D1 database.

For the REST API, use an API token with D1:Edit permission.

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{"read_replication": {"mode": "auto"}}"

Disable read replication

With the REST API, set read_replication.mode: disabled to disable read replication on a D1 database.

For the REST API, use an API token with D1:Edit permission.

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{"read_replication": {"mode": "disabled"}}"

Check if read replication is enabled

GET D1 database REST endpoint returns if read replication is enabled or disabled.

For the REST API, use an API token with D1:Read permission.

curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \
-H "Authorization: Bearer $TOKEN"
  • Check the read_replication property of the result object
    • "mode": "auto" indicates read replication is enabled
    • "mode": "disabled" indicates read replication is disabled

Start a Session without constraints

To create a Session from any available database version, use withSession() without any parameters, which will route the first query to any database instance, either the primary database instance or a read replica.

const session = env.DB.withSession()
// query executes on either primary database or a read replica
const result = await session
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()
  • withSession() is the same as withSession("first-unconstrained")
  • This approach is best when your application does not require the latest database version. All queries in a Session ensure sequential consistency.
  • Refer to the D1 Workers Binding API documentation.

Start a Session with all latest data

To create a Session from the latest database version, use withSession("first-primary"), which will route the first query to the primary database instance.

// synchronous
const session = env.DB.withSession(`first-primary`)
const result = await session // query executes on primary database
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()
  • This approach is best when your application requires the latest database version. All queries in a Session ensure sequential consistency.
  • Refer to the D1 Workers Binding API documentation.

Start a Session from previous context (bookmark)

To create a new Session from the context of a previous Session, pass a bookmark parameter to guarantee that the Session starts with a database version that is at least as up-to-date as the provided bookmark.

const bookmark = request.headers.get('x-d1-bookmark') ?? 'first-unconstrained';
const session = env.DB.withSession(bookmark)
const result = await session
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run()
// store bookmark for a future Session
response.headers.set('x-d1-bookmark', session.getBookmark() ?? "")
  • Starting a Session with a bookmark ensures the new Session will be at least as up-to-date as the previous Session that generated the given bookmark.
  • Refer to the D1 Workers Binding API documentation.

Check where D1 request was processed

To see how D1 requests are processed by the addition of read replicas, served_by_region and served_by_primary fields are returned in the meta object of D1 Result.

const result = await env.DB.withSession()
.prepare(`SELECT * FROM Customers WHERE CompanyName = 'Bs Beverages'`)
.run();
console.log({
servedByRegion: result.meta.served_by_region ?? "",
servedByPrimary: result.meta.served_by_primary ?? "",
});
  • served_by_region and served_by_primary fields are present for all D1 remote requests, regardless of whether read replication is enabled or if the Sessions API is used. On local development, npx wrangler dev, these fields are undefined.

Read replica locations

Currently, D1 automatically creates a read replica in every supported region, including the region where the primary database instance is located. These regions are:

  • ENAM
  • WNAM
  • WEUR
  • EEUR
  • APAC
  • OC

Observability

To see the impact of read replication and check the how D1 requests are processed by additional database instances, you can use:

  • The meta object within the D1Result return object, which includes new fields:
    • served_by_region
    • served_by_primary
  • The Cloudflare dashboard, where you can view your metrics breakdown by region that processed D1 requests.

Known limitations

There are some known limitations for D1 read replication.

  • The lifecycle of a read replica is tied to the lifecycle of the primary database instance. For example, if the primary database instance is inactive, the read replicas are inactive too.
  • TBC

Background information

Replica lag and consistency model

To account for replica lag, it is important to consider the consistency model for D1. A consistency model is a logical framework that governs how a database system serves user queries (how the data is updated and accessed) when there are multiple database instances. Different models can be useful in different use cases. Most database systems provide read committed, snapshot isolation, or serializable consistency models, depending on their configuration.

D1 read replication offers sequential consistency. D1 creates a global order of all operations which have taken place on the database, and can identify the latest version of the database that a query has seen, using bookmarks. It then serves the query with a database instance that is at least as up-to-date as the bookmark passed along with the query to execute.

Sequential consistency has properties such as:

  • Monotonic reads: If you perform two reads one after the other (read-1, then read-2), read-2 cannot read a version of the database prior to read-1.
  • Monotonic writes: If you perform write-1 then write-2, all processes observe write-1 before write-2.
  • Writes follow reads: If you read a value, then perform a write, the subsequent write must be based on the value that was just read.
  • Read my own writes: If you write to the database, all subsequent reads will see the write.

Benefits of read replication

A system with multiple read replicas located around the world improves the performance of databases:

  • The read throughput increases by distributing load across multiple replicas. Since multiple database instances are able to serve read-only requests, your application can serve a larger number of queries at any given time.
  • The query latency decreases for users located close to the read replicas. By shortening the physical distance between a the database instance and the user, read query latency decreases, resulting in a faster application.

Supplementary information

You may wish to refer to the following resources: