Skip to content

D1

D1 is a SQL database Cloudflare runs for you, built on SQLite. You write ordinary SQL against tables you define, and there’s no server to size: you pay for the rows you read and write and the data you store.

A D1 database has one primary copy, in one location. Every write goes to it, in order, so there’s no conflict to resolve. Workers in any city reach it through a binding.

writesreadsWorkerMadridprimaryweurread replicaapacWorkerTokyo
Every write goes to the primary, in one location. With read replication on, a Worker far away can read from a replica near it.

A database holds up to 10 GB. That’s a lot for one app’s data, and a hint about how D1 is meant to be used: many small databases, one per app, per tenant or per user, rather than one huge one.

src/database.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
export const Database = Cloudflare.D1.Database("Database", { migrations: "./migrations" });

The primary is placed near the first requests. Set primaryLocationHint, like "weur", to choose. To keep its data inside a legal boundary, set jurisdiction.

migrations points at a directory of .sql files. Each deploy applies the ones that haven’t run yet, in the order of their numeric prefix, 0001_, 0002_:

migrations/0001_users.sql
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE);

Applied files are recorded in an __alchemy_migrations table. A database already migrated with wrangler or drizzle-kit is picked up where it left off. To generate migrations from a schema instead of writing them, see Drizzle on D1.

Bind the database with D1.QueryDatabase, and prepare statements against it:

src/worker.ts
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
const db = yield* Cloudflare.D1.QueryDatabase(Database);
return {
fetch: Effect.gen(function* () {
const users = yield* db.prepare("SELECT * FROM users WHERE id = ?").bind(1).all();
return yield* HttpServerResponse.json(users);
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding)),
);

prepare and bind only build the statement. all, first and run send it, and return Effects. batch sends several statements in one round trip, applied together or not at all.

For SQL written as a template, with values parameterized for you and typed errors, wrap the binding in an Effect SQL client:

src/worker.ts
const d1 = yield* Cloudflare.D1.QueryDatabase(Database);
const sql = yield* SQL.D1(d1);
const users = yield* sql`SELECT * FROM users WHERE id = ${id}`;

SQL comes from @alchemy.run/cloudflare/D1/SQL. The client implements Effect’s SqlClient, so code written against SqlClient.SqlClient runs on D1 unchanged. See Effect SQL on D1.

With read replication on, D1 keeps read-only copies of the database in other regions, and serves reads from the nearest one:

src/database.ts
export const Database = Cloudflare.D1.Database("Database", {
migrations: "./migrations",
readReplication: { mode: "auto" },
});

A replica can be slightly behind the primary. To read your own writes, use Cloudflare’s sessions API on the native database, which the client’s raw escape hatch gives you: a session carries a bookmark from each write, and later reads in the session wait until their replica has caught up to it.

Use D1 for relational data owned by one app or one tenant. For data that’s read everywhere and rarely written, KV is faster. For state that belongs to one entity and must be consistent per request, a Durable Object’s own SQLite fits better. For Postgres or MySQL you already run, or need, connect through Hyperdrive.