DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Cloudflare D1: SQLite at the Edge After 6 Months in Production

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
]);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)