D1 SQL Database
Learning Focus
By the end of this lesson you will understand how D1 provides serverless SQL at the edge, how to create databases, run queries from Workers, and what the free tier includes.
What Is D1?
D1 is Cloudflare's serverless SQL database built on SQLite. It runs at the edge, close to your Workers and users, providing low-latency SQL queries without managing database servers.
flowchart LR
WORKER["Worker\n(Edge)"] -->|SQL Query| D1["D1 Database\n(SQLite at Edge)"]
D1 -->|Results| WORKER
style WORKER fill:#f6821f,color:#fff,stroke:#e5711e
style D1 fill:#7c3aed,color:#fff,stroke:#6d28d9
Free Tier
| Resource | Free Plan |
|---|---|
| Rows read | 5 million per day |
| Rows written | 100,000 per day |
| Storage | 5 GB total (across all databases) |
| Databases | 50,000 (10 per account on free, more on paid) |
Getting Started
Create a Database
# Create a new D1 database
wrangler d1 create my-database
# Output:
# Created D1 database 'my-database'
# Database ID: <database-id>
wrangler.toml
name = "my-worker"
main = "src/index.ts"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "<database-id>"
Define Schema
Create a migration
wrangler d1 migrations create my-database init
migrations/0001_init.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
# Apply migration
wrangler d1 migrations apply my-database
Query from a Worker
src/index.ts
export interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// List all users
if (url.pathname === "/users" && request.method === "GET") {
const { results } = await env.DB.prepare(
"SELECT * FROM users ORDER BY created_at DESC"
).all();
return Response.json(results);
}
// Create a user
if (url.pathname === "/users" && request.method === "POST") {
const { email, name } = await request.json() as any;
const result = await env.DB.prepare(
"INSERT INTO users (email, name) VALUES (?, ?)"
).bind(email, name).run();
return Response.json({ id: result.meta.last_row_id }, { status: 201 });
}
// Get a user by ID
if (url.pathname.startsWith("/users/")) {
const id = url.pathname.split("/")[2];
const user = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(id).first();
if (!user) return new Response("Not Found", { status: 404 });
return Response.json(user);
}
return new Response("Not Found", { status: 404 });
},
};
Querying D1 from CLI
Run queries directly
# Execute a query
wrangler d1 execute my-database --command "SELECT * FROM users"
# Execute from a file
wrangler d1 execute my-database --file ./seed.sql
When to Use D1
| Use Case | D1 | Workers KV | Durable Objects |
|---|---|---|---|
| Relational data | ✅ Best choice | ❌ | ❌ |
| Complex queries (JOIN, GROUP BY) | ✅ Full SQL | ❌ | ❌ |
| Simple key-value | Overkill | ✅ Best choice | ❌ |
| Real-time counters | ❌ | ❌ | ✅ Best choice |
| Blog / CMS data | ✅ | ❌ | ❌ |
| User profiles | ✅ | ❌ | ❌ |
Key Takeaways
- D1 is serverless SQLite at the edge — no servers to manage, no connection pools.
- Free tier: 5M rows read/day, 100k writes/day, 5 GB storage.
- Full SQL support — JOINs, indexes, transactions, and migrations.
- Use prepared statements with
bind()to prevent SQL injection. - D1 is best for relational data — use KV for simple key-value, Durable Objects for real-time.
What's Next
- Continue to Durable Objects to learn about stateful compute with strong consistency.