Cloudflare D1: The Edge SQLite Database Guide (2026)
A practitioner's look at Cloudflare D1, the serverless SQLite database built for Workers, covering setup, read replication, limits, and fit.
Key takeaways
A practitioner's look at Cloudflare D1, the serverless SQLite database built for Workers, covering setup, read replication, limits, and fit.
If you already run your app on Cloudflare Workers, reaching for a traditional database means adding a network hop to some region far from your users. D1 is Cloudflare's answer: a serverless SQLite database that lives inside the same platform as your Workers, with read replicas placed near where your traffic actually is. This guide walks through what D1 is, how it works, and where it earns a spot in your stack.
For the broader landscape, see our serverless and edge databases guide.
What D1 is#
D1 is a managed SQLite database exposed to Cloudflare Workers through a binding. You don't provision servers, manage connection pools, or pick a region for compute. You create a database, bind it to a Worker, and query it with prepared statements. Under the hood each database is a SQLite instance, so you get real SQL, transactions, indexes, and the SQLite dialect you already know.
The part that makes it an edge database rather than just hosted SQLite is read replication. D1 keeps a primary copy plus read replicas distributed across Cloudflare's network. Reads can be served from a replica close to the user, while writes route to the primary. That shape is the whole pitch: cheap, fast reads near the reader, with a single authoritative writer.
How it works#
The mental model has three pieces.
Primary plus replicas: Every write goes to the primary. Replicas asynchronously catch up. A read hitting a nearby replica might be slightly behind the primary, which matters for consistency (more on that below).
The Workers binding model: Your Worker never opens a raw connection. Instead, wrangler.toml declares a binding, and at runtime that binding shows up on env as an object with a query API. Cloudflare handles routing your query to the right instance.
Prepared statements: You build a statement, bind parameters, and execute. The API is small: prepare(), bind(), then all(), first(), or run().
Getting started with wrangler#
Create a database from the CLI:
npx wrangler d1 create my-app-db
# Copy the printed database_id into wrangler.toml
npx wrangler d1 migrations create my-app-db create_users
Add the binding and point at a migrations directory. Verify field names against current Cloudflare docs, since the config format shifts between wrangler versions:
# wrangler.toml
name = "my-app"
main = "src/index.ts"
compatibility_date = "2026-07-01"
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
migrations_dir = "migrations"
Write a migration as plain SQL, then apply it:
-- migrations/0001_create_users.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_users_email ON users(email);
npx wrangler d1 migrations apply my-app-db --local # local dev
npx wrangler d1 migrations apply my-app-db --remote # production
Now query from the Worker. The binding name in wrangler.toml becomes the property on env:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const email = url.searchParams.get("email") ?? "";
const { results } = await env.DB
.prepare("SELECT id, email, created_at FROM users WHERE email = ?")
.bind(email)
.all();
return Response.json(results);
},
};
Always bind parameters rather than interpolating strings into SQL. It's the same injection story as any SQL database.
Read replication and the Sessions API#
Async replication buys you speed at the cost of a consistency wrinkle: right after a write, a read from a lagging replica might not see it. For a user who just updated their profile and reloads the page, that reads as a bug.
D1's Sessions API addresses this with read-your-writes semantics. You start a session, and D1 tracks a bookmark representing how current your reads need to be. Follow-up reads in that session are guaranteed to see at least the data your session has already written, routing to a replica that's caught up (or to the primary) as needed. The rough shape:
const session = env.DB.withSession("first-primary");
await session.prepare("INSERT INTO users (email) VALUES (?)").bind(email).run();
const { results } = await session
.prepare("SELECT * FROM users WHERE email = ?")
.bind(email)
.all(); // sees the insert above
Check the current method names and session modes against Cloudflare docs before shipping, as this API has evolved. The key idea: use sessions when a request does a write and then reads its own result. Pure read traffic can skip sessions and take the fastest nearby replica.
Limits, pricing, and local dev#
D1 has real ceilings. There's a maximum database size (in the low tens of gigabytes per database, verify against current Cloudflare docs), plus per-query limits on rows returned and statement complexity. If you outgrow one database, the intended pattern is many databases (for example, one per tenant) rather than one giant instance.
Pricing follows a usage model: rows read and rows written, plus storage, with a free tier and paid tiers on the Workers plan. Because billing counts rows touched, an unindexed query that scans a big table costs you twice, in latency and in dollars. Treat indexes as a cost control, not just a performance tuning step. Confirm the exact numbers against current Cloudflare pricing, since they change.
Local development is genuinely good. Wrangler runs D1 on a local SQLite file via miniflare, so wrangler dev gives you a working database with no network round trip. Apply migrations with --local, iterate, and only touch --remote when you're ready. The same binding code runs in both places.
When to use D1#
D1 is a strong fit when:
- Your app already lives on Cloudflare Workers and you want to keep everything on one platform.
- Traffic is read-heavy and globally distributed, so replicas near users pay off.
- Your data model is comfortable in SQLite and fits within the size limits.
- You want low operational overhead and cheap-to-zero idle cost.
D1 is the wrong tool when:
- You have write-heavy workloads. A single primary caps write throughput, and async replication doesn't help writes.
- Your dataset is large, well past the per-database size limit, with no clean way to shard.
- You need Postgres-specific features: rich types, extensions like PostGIS, stored procedures, or advanced concurrency.
- You need strong global consistency on every read without threading sessions through your code.
If you're weighing alternatives, our Turso vs Cloudflare D1 comparison digs into the other major edge-SQLite option.
The call we'd make#
For a Workers app that's read-dominated and happy with SQLite, D1 is close to a default. The binding model is clean, local dev is painless, and read replication plus the Sessions API cover the common consistency trap without a lot of ceremony. The moment your workload turns write-heavy, needs Postgres features, or pushes past the size limits, stop and reach for a different database. D1 is a sharp tool for a specific shape of app, and it's most of the way to great when that shape matches yours. Verify the specific limits, pricing, and API names against current Cloudflare docs before you commit, because this product moves fast.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Software Supply Chain Attacks: How They Work & Defenses
A practical tour of how software supply chain attacks reach your build, and the controls that actually stop them.
How DNS Works (Explained Simply)
A developer-friendly walk through DNS resolution, record types, TTL, and the caching quirks that cause real production bugs.
More from Cloud
Explore more articles in this category
The Cheapest Way to Centralize Logs at Scale
Cutting a log bill is not a procurement exercise. It is four decisions about what you drop at the agent, what you index, how long you keep it, and what you never send at all.
AWS Raised GPU Prices Twice in 2026: What to Do About It
EC2 Capacity Blocks went up around 15% in January and again in July. The increases track the memory shortage, and they change which GPU cloud is actually cheapest for your workload.
The RAM Shortage Is Now a Line Item on Your Cloud Bill
Memory makers moved their wafers to HBM for AI accelerators, and DDR5 spot prices tripled. Here is how that reaches your instance bill and what actually reduces the exposure.
You might have missed
Evergreen posts worth revisiting.