Both put SQLite near your users, but they solve replication and write latency very differently. We ran the same schema on both for a month and picked one.
We were building a feature-flag service that reads constantly and writes rarely, and we wanted the reads to be fast everywhere, not just near us-east-1. That's the sweet spot both Turso and Cloudflare D1 aim at: SQLite, but distributed close to users. We ran the same schema and workload on both for four weeks. They look similar on the tin and behave quite differently once traffic hits.
D1 is Cloudflare's managed SQLite, built on their storage layer. A database has one primary location where writes land, and D1 read replicas (now generally available) serve reads from other regions with the primary asynchronously replicating out. You talk to it through a Worker binding, not a wire protocol. That binding is the point: D1 assumes you're inside the Cloudflare Workers runtime.
export default {
async fetch(request, env) {
const { results } = await env.DB
.prepare("SELECT enabled FROM flags WHERE key = ?")
.bind("checkout_v2")
.all();
return Response.json(results);
},
};
Turso is built on libSQL, an open fork of SQLite. Its model is embedded replicas: your application (or a sidecar) holds a full local copy of the database on disk and reads hit that local file at microsecond latency, while writes go to a primary and sync back. You connect over a normal client library from anywhere, not just an edge runtime.
import libsql_experimental as libsql
conn = libsql.connect(
"flags.db",
sync_url="libsql://flags-acme.turso.io",
auth_token=TOKEN,
)
conn.sync() # pull latest from primary
rows = conn.execute(
"SELECT enabled FROM flags WHERE key = ?", ("checkout_v2",)
).fetchall()
That embedded-replica model is Turso's headline trick. Reads don't cross the network at all once synced. For our read-heavy flag service, local reads measured under 1ms, versus D1 read-replica reads landing around 8-25ms depending on region.
Neither makes writes free. Both have a single write primary, so a write from the far side of the world pays a round trip. In our tests, writes to D1 from a Worker in Europe hitting a US primary ran 60-110ms. Turso writes from an embedded replica were similar, 70-120ms, because the write still travels to the primary and the local replica only reflects it after the next sync.
The important nuance for Turso: after a write, your local replica is stale until you call sync() or hit the periodic sync interval. If your app writes a flag and immediately reads it back on the same replica, you can read the old value. You either sync after writes or read-your-writes through the primary connection. D1 read replicas have the same eventual-consistency caveat; a Sessions API exists to pin a request to see its own writes. Both make you think about consistency; neither hides it.
D1 bills on rows read and rows written plus storage. The free tier is generous (5M rows read/day), and at our volume the paid usage was a few dollars a month. Rows-read billing is worth watching though: a bad query that scans a big table on every request runs up "rows read" fast, and we tripped a bill spike once with an unindexed lookup.
Turso bills on rows read/written and total storage across replicas, with a free tier around 500 databases and 9GB. Its per-database model is interesting: you can spin up a database per tenant cheaply, which D1 also allows but Turso leans into harder. For multi-tenant SaaS where each customer wants isolation, Turso's database-per-tenant story is genuinely nicer.
D1 only runs on Cloudflare. Full stop. Your data and your access path are both tied to Workers. That's fine if you're already all-in on Cloudflare, and painful if you ever want out.
Turso is libSQL, open source, and you can self-host the server. The embedded replica is just a SQLite file you can copy. If portability matters, or if you want the same DB reachable from a Fly.io box and a Lambda and a Worker, Turso is the less trapped choice.
If your whole stack already lives in Cloudflare Workers and you want the least-effort managed option with a binding that just works, use D1. The read replicas closed most of its old latency gap and the ergonomics inside Workers are hard to beat. If you're read-dominated and want true local-file read latency, or you need database-per-tenant, or you can't stomach single-vendor lock-in, use Turso. We shipped the flag service on Turso specifically for the sub-millisecond embedded reads and the option to self-host, but I'd have picked D1 without hesitation if we were already a Workers shop.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Least privilege fails when it's a one-time audit that locks things down until something breaks, then gets reverted. The iterative, log-driven approach that tightens permissions safely — and the policies we stopped writing by hand.
A bad deploy used to mean a pager at 2am and a manual rollback. Now Argo Rollouts watches the error rate and aborts the canary itself before anyone wakes up.
Explore more articles in this category
We ran secrets three different ways across AWS, GCP, and Vault. External Secrets Operator gave us one Kubernetes-native workflow. Here's the setup and the gotchas.
Moving our fleet from x86 to Graviton promised 20% savings. We got 31%, but only after fixing native dependencies, a broken base image, and one nasty perf regression.
A p99 that jumped to 3.4 seconds during traffic ramps turned out to be cold starts. Here's how we measured them properly and cut the tail, with real init timings.
Evergreen posts worth revisiting.