Cloudflare D1: SQLite at the Edge After 6 Months in Production
D1 went GA in late 2024. I've been running it in production for six months across two projects — one high-read SaaS, one write-heavy background job system. Here's what actually happened.
What D1 Actually Is
D1 is SQLite running on Cloudflare's edge network. Your database sits physically close to your users. Reads are local. Writes replicate.
That last part matters. D1 uses a primary-replica model: one primary node accepts writes, changes propagate to read replicas globally. The propagation latency is ~350ms in practice — not instantaneous.
The Good Stuff
Zero Cold Starts on Reads
SQLite on the edge means your Worker hits a local replica. P99 read latency on our dashboard queries went from 45ms (Turso + Vercel Edge) to 8ms with D1. That's real.
Drizzle ORM Works Perfectly
import { drizzle } from 'drizzle-orm/d1';
import { users } from './schema';
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB);
const allUsers = await db.select().from(users).limit(50);
return Response.json(allUsers);
}
};
No connection pooling, no pg driver. Just SQLite pragmas and Drizzle.
Free Tier Is Genuinely Useful
- 5M rows read/day
- 100K rows written/day
- 5GB storage
For internal tooling or early-stage SaaS, you won't pay a cent.
The Hard Parts
Write Latency Is Real
Writes go to the primary, then replicate. If your user is in Sydney and your primary is in Virginia, write latency is 200-300ms. That's fine for form submissions. It's not fine for real-time collaborative apps.
My rule: D1 is ideal when reads >> writes. Content sites, dashboards, read-heavy SaaS — great. Multiplayer apps, high-frequency writes — look elsewhere.
No RETURNING in Older Cloudflare Builds
SQLite 3.35+ supports RETURNING. Some D1 workers on older runtimes don't. This bit me:
-- Sometimes fails on D1:
INSERT INTO jobs (id, status) VALUES (?, ?) RETURNING id;
Workaround: INSERT, then SELECT last_insert_rowid().
await db.insert(jobs).values({ id: jobId, status: 'pending' });
const [{ id }] = await db.execute('SELECT last_insert_rowid() as id');
Update your compatibility_date to 2024-09-23 or later to get full SQLite 3.46.
Migrations Are Manual-ish
D1 has a wrangler d1 migrations apply command but no auto-detection. You write .sql files, version them yourself. Drizzle's drizzle-kit can generate them, but the apply step is Wrangler:
# Generate migration
npx drizzle-kit generate:sqlite
# Apply to D1
npx wrangler d1 migrations apply my-database
Not painful, just more manual than Prisma Migrate.
Batch API Is Your Friend for Writes
D1's batch API lets you send multiple statements in one round trip:
const results = await env.DB.batch([
env.DB.prepare('INSERT INTO events VALUES (?, ?, ?)').bind(id, type, ts),
env.DB.prepare('UPDATE counters SET val = val + 1 WHERE key = ?').bind(type),
]);
This matters for write-heavy operations. Single writes in a loop will hammer your latency budget.
D1 vs Turso vs PlanetScale
| D1 | Turso | PlanetScale | |
|---|---|---|---|
| Protocol | HTTP | libSQL | MySQL |
| Edge reads | ✅ native | ✅ replicas | ✅ replicas |
| Write latency | ~200ms global | ~150ms | ~100ms |
| Free tier | 5M reads/day | 500 DBs, 9GB | 5GB |
| Best for | CF Workers | Any platform | MySQL workloads |
If you're already on Cloudflare Workers, D1 is the zero-friction choice. If you're on Vercel or Fly, Turso's libSQL driver is more portable.
Schema Design Lessons for D1
1. Composite indexes matter more at the edge.
SQLite's query planner is conservative. Without the right indexes, even small tables scan fully. For D1 specifically:
CREATE INDEX idx_events_user_ts ON events(user_id, created_at DESC);
Always index your primary filter + sort columns together.
2. Keep rows narrow.
D1 replicates rows. Wide rows with blob columns slow replication and eat your storage budget. Store blobs in R2, reference by key.
3. Avoid long transactions.
D1 doesn't support long-running transactions across Worker invocations. Design for short, atomic writes.
The Verdict After 6 Months
D1 is production-ready for the right workload. Read-heavy, globally distributed, on Cloudflare Workers — it's excellent. Sub-10ms reads with zero infrastructure management is hard to beat.
For write-heavy or latency-sensitive write paths, you'll feel the replication lag. Use D1 for state that can tolerate eventual consistency, keep synchronous writes small.
The free tier alone makes it worth evaluating for any new Workers project.
Building a SaaS on Cloudflare Workers or Next.js and need a production-ready foundation? The AI SaaS Starter Kit ships with D1 schema patterns, Drizzle setup, and auth wired up for $99.
Top comments (0)