Edge Databases: Turso and D1
Edge compute without edge data means every Worker still calls home to us-east-1 Postgres for a feature flag lookup. Turso and Cloudflare D1 bring SQLite-shaped databases to the edge — small, fast, SQL-familiar — with different replication stories and platform lock-in trade-offs. Picking between them is less about SQL syntax (both speak SQLite) and more about where your compute runs, how much replication lag you tolerate, and whether you need libSQL outside Cloudflare's ecosystem.
SQLite at the edge — why it works
SQLite is embedded, single-file, and battle-tested. At the edge you typically do not mount a filesystem per request; instead managed services host the database file and replicate read copies to PoPs. Writes go to a primary; reads can hit nearest replica. Workloads that fit:
- Feature flags and tenant config
- Edge session and rate-limit counters (with care)
- Content catalogs synced periodically
- User preference caches
Workloads that do not fit: high-frequency financial ledger writes, cross-row transactional inventory at scale.
Cloudflare D1
D1 is SQLite integrated into Workers via bindings:
export default {
async fetch(request, env) {
const { results } = await env.DB.prepare(
`INSERT INTO events (type, payload) VALUES (?, ?) RETURNING id`
).bind('page_view', JSON.stringify({ path: '/' })).all();
return Response.json({ id: results[0].id });
},
};
Schema migrations run through wrangler:
npx wrangler d1 execute my-db --file=./schema.sql
npx wrangler d1 migrations apply my-db
D1 offers:
- Native Workers integration — no HTTP hop for queries inside the same request
- Time Travel backups — point-in-time recovery on paid tiers
- Global read routing — replicas serve reads closer to users; understand lag for your tier
Limits: database size caps, query duration bounds, and platform coupling — D1 is accessed most naturally from Workers.
Turso and libSQL
Turso builds on libSQL, an open-source fork of SQLite with extensions for replication and remote access:
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
const rs = await client.execute({
sql: 'SELECT enabled FROM flags WHERE key = ?',
args: ['dark_mode'],
});
Embedded replicas — sync a local SQLite file from Turso for read-heavy edge or mobile:
import { createClient } from '@libsql/client';
const client = createClient({
url: 'file:local.db',
syncUrl: process.env.TURSO_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
await client.sync(); // pull updates from primary
Turso runs on Fly.io regions, supports many client languages, and works outside Cloudflare — Vercel functions, mobile apps, local dev with the same URL.
Consistency and replication lag
Both systems are eventually consistent on reads from replicas. After a write:
// Risky at edge without sticky routing
await env.DB.prepare('UPDATE counter SET v = v + 1').run();
const row = await env.DB.prepare('SELECT v FROM counter').first();
// row.v may be stale if read hit a lagging replica
Mitigations:
- Read from primary for read-your-writes paths (Turso
syncUrlwrite mode; D1 session APIs where available) - Design idempotent writes
- Cache bust with version tokens in KV
Choosing between them
| Factor | D1 | Turso |
|---|---|---|
| Primary compute | Cloudflare Workers | Multi-cloud, mobile, Fly |
| Client access | Workers bindings | HTTP, libSQL wire, embedded |
| Ops model | Fully managed in CF dashboard | Turso cloud or self-host libSQL |
| Ecosystem lock-in | Higher | Lower |
If your stack is Workers-only and you want zero external vendors, D1 is the path of least resistance. If you need the same database from Workers, a Node API in AWS, and an iOS app with embedded sync, Turso's libSQL client story is stronger.
Schema and migration discipline
Edge SQLite still needs migrations in Git:
-- migrations/001_init.sql
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY,
plan TEXT NOT NULL DEFAULT 'free',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
Test migrations against a copy of production size data locally. SQLite's flexible typing helps prototypes but hurts if you skip constraints — enforce NOT NULL and CHECK at schema time.
Test Turso replication lag for read-your-writes paths — edge reads may serve stale data for seconds after write to primary.
Turso embedded replicas
const client = createClient({
url: "file:local.db",
syncUrl: process.env.TURSO_SYNC_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
await client.sync(); // pull updates before read
Edge reads local SQLite, syncs writes to primary — expect eventual consistency on read-your-writes without sync call.
Common production mistakes
Teams get databases turso d1 wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Production implementations of databases turso d1 fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.
Debugging and triage workflow
When databases turso d1 misbehaves in production, work top-down instead of guessing:
- Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
- Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
- Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
- Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
- Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
- Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.
Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.
Resources
- Cloudflare D1 documentation
- Turso documentation
- libSQL project
- @libsql/client npm package
- SQLite WAL mode and concurrency
Frequently asked questions
What is the main difference between Turso and Cloudflare D1?
Turso is libSQL (SQLite fork) with primary/replica replication to regions worldwide and clients in many languages via HTTP and embedded replicas. D1 is Cloudflare-native SQLite integrated into Workers bindings with read replication still evolving. Turso fits multi-platform edge apps; D1 fits Workers-first stacks already on Cloudflare.
Can I run transactions on edge SQLite databases?
Both support SQLite transactions on a single database connection. Distributed transactions across regions are not SQLite's model — you write to the primary (Turso) or leader (D1) and reads hit replicas with replication lag. Design for idempotent writes and read-your-writes tolerance or route writes through the primary region.
When should I not use edge SQLite?
Skip edge SQLite for heavy write throughput (thousands of writes/sec sustained), complex multi-tenant isolation requiring row-level security at scale, or analytics over terabytes. Use Postgres or a warehouse for those. Edge SQLite wins for config, feature flags, session metadata, and read-heavy app state close to users.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →