Skip to main content

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

ResourceFree Plan
Rows read5 million per day
Rows written100,000 per day
Storage5 GB total (across all databases)
Databases50,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 CaseD1Workers KVDurable Objects
Relational data✅ Best choice
Complex queries (JOIN, GROUP BY)✅ Full SQL
Simple key-valueOverkill✅ 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.