Cloudflare D1 is not just another managed SQLite database; it’s a distributed SQLite database designed to live alongside your Workers, making latency a relic of the past.
Let’s see it in action. Imagine you have a simple "todos" application.
First, you need to create a D1 database:
wrangler d1 create my-todo-db
This command provisions a new D1 database instance. You’ll get a database ID back, which you’ll use to interact with it.
Next, you create a table in that database:
wrangler d1 execute <YOUR_D1_DATABASE_ID> --file schema.sql
Where schema.sql contains:
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
);
Now, let’s write a Worker to interact with this database. In your wrangler.toml, you’ll bind the D1 database to a variable:
name = "todo-worker"
main = "src/index.js"
compatibility_date = "2023-10-19"
[[d1_databases]]
binding = "DB"
database_id = "<YOUR_D1_DATABASE_ID>"
And in your src/index.js:
export default {
async fetch(request, env, ctx) {
if (request.method === "POST") {
const { task } = await request.json();
await env.DB.prepare("INSERT INTO todos (task) VALUES (?)")
.bind(task)
.run();
return new Response("Todo created", { status: 201 });
}
if (request.method === "GET") {
const { results } = await env.DB.prepare("SELECT * FROM todos").all();
return Response.json(results);
}
return new Response("Method not allowed", { status: 405 });
},
};
When you deploy this Worker, requests to / will hit your Worker. For POST requests, it takes a JSON body with a task and inserts it into the todos table. GET requests will fetch all todos. Because the D1 database is collocated with your Worker, these operations are incredibly fast. There’s no network round trip to a separate database server.
The magic here is how D1 handles distribution. It’s not a single SQLite file on a server. Instead, Cloudflare shards your data across multiple locations. When your Worker executes a query, Cloudflare’s edge network routes that query to the nearest replica of your data. This achieves low latency for reads and writes globally. The wrangler d1 execute command, for instance, isn’t just running SQL; it’s intelligently distributing schema changes.
The env.DB object that your Worker code sees is a local representation of the D1 database. When you call .prepare(), you’re creating a query object. .bind() is crucial for security and performance, preventing SQL injection and allowing SQLite to optimize query plans. .run() executes a statement that doesn’t return rows (like INSERT, UPDATE, DELETE), while .all() executes a query and returns all results.
What most people miss is how D1 handles transactions across distributed replicas. When you initiate a transaction with env.DB.transaction(), D1 doesn’t just start a local SQLite transaction. It coordinates with multiple replicas to ensure atomicity and isolation, using a consensus protocol under the hood. This means you can rely on transactional integrity even though your data is physically spread across the globe.
The next step is to explore D1’s backup and restore capabilities.