· turso / cloudflare / d1
Turso vs Cloudflare D1: Which Edge SQLite Should You Pick?
D1 is the obvious choice if you live in Cloudflare Workers. Turso is the right pick everywhere else: Node.js, Deno, Bun, and Vercel. Here is the data.
By toolchew
2,004 words · 11 min read
Use Cloudflare D1 if your entire stack runs on Cloudflare Workers and you want the simplest possible database setup. Use Turso if you need your database to work in Node.js, Bun, Deno, Vercel, or any runtime outside Cloudflare. That is the core split. Everything below is the evidence.
Who this is for
Developers evaluating edge or serverless SQLite in 2026. If you are already deep in one platform and happy, this article won’t change your mind — but it will tell you what you are giving up.
TL;DR
| Turso | Cloudflare D1 | |
|---|---|---|
| Free tier reads | 500M rows/month | 5M rows/day (~150M/month) |
| Entry paid plan | $4.99/month | $5/month (Workers Paid base) |
| Write topology | Single-region primary + optional replicas | Single-region primary |
| SDK portability | Node, Bun, Deno, edge runtimes, mobile | Cloudflare Workers binding only |
| ORM support | Drizzle ✓, Prisma ✓ (Migrate ✗) | Drizzle ✓, Prisma Preview (transactions ✗) |
| Vendor lock-in | Low — libSQL is open-source | High — Workers binding is proprietary |
Pricing
D1 free tier runs on daily limits: 5M rows read and 100K rows written per day, 5 GB total storage, 10 databases max. That caps you at roughly 150M reads per month if traffic is even. The paid plan adds D1 at no extra base cost on top of the $5/month Workers Paid subscription: 25 billion rows read and 50 million rows written per month are included. Overage runs $0.001 per million reads and $1.00 per million writes. Storage beyond the first 5 GB costs $0.75/GB per month.
Turso free tier is measured monthly: 500M rows read, 10M rows written, 5 GB storage, 100 databases. The Developer plan at $4.99/month raises that to 2.5B reads, 25M writes, 9 GB storage, and unlimited databases. Scaler at $24.92/month and Pro at $416.58/month add more headroom on reads, writes, storage, and point-in-time restore window.
For read-heavy apps, Turso’s free tier is meaningfully more generous — 500M reads per month versus D1’s effective ceiling of about 150M. At paid scale, D1’s 25B included reads for $5/month base is hard to beat. Both platforms charge $1.00 per million writes over the included limit, which adds up fast for write-heavy workloads.
One sharp edge in D1: the free tier’s 50 queries per Worker invocation cap. Complex pages that issue many small queries — fetching a user, their preferences, their feed — can hit this before hitting row limits. The paid plan raises it to 1,000, which is rarely binding.
Data model: libSQL vs managed SQLite
Turso runs libSQL, an open-source SQLite fork maintained by Turso because SQLite’s upstream does not accept external contributions. It is 100% SQLite-compatible — same file format, same API, same SQL dialect. Beyond vanilla SQLite, libSQL adds native vector types (F32_BLOB with DiskANN indexing), a native replication protocol over gRPC, and browser/WASM support.
Turso is in the middle of a bigger transition: the new Turso Database engine (formerly Limbo) is a complete Rust rewrite of SQLite with concurrent writes and async I/O, currently in beta. The C-based libSQL engine powers Turso Cloud today. New projects that need async I/O or concurrent writes can opt into the beta via @tursodatabase/database.
D1 runs Cloudflare-managed SQLite-compatible storage. Cloudflare does not prominently advertise a pinned version; community research pegs it at SQLite 3.46 with compatibility_date >= 2024-09-23, updated through the compatibility date system rather than a fixed pin. D1 does not support interactive transactions — there is no BEGIN/COMMIT/ROLLBACK SQL syntax. What it does have is the batch() API, which executes a list of statements as an atomic SQL transaction and rolls back the entire batch on failure. What you cannot do is open a transaction, inspect intermediate results, then commit or roll back conditionally.
Replication topology
Turso uses a group model: one primary region, zero or more replica locations. Reads serve from the nearest replica. Writes go to the primary; replicas forward writes automatically. Available regions are Tokyo, Mumbai, Ireland, Virginia, Ohio, and Oregon — all on AWS.
There is one significant caveat for new users: geographic replication (edge replicas) has been discontinued. Turso says 70% of users never created a replica, so the feature is being retired in favor of a better data-locality solution that is still in development. Existing paid customers retain access; new accounts start with a single-region primary.
Turso also had embedded replicas — a local SQLite file synced to Turso Cloud with microsecond local reads. These require a persistent filesystem, which rules them out on Cloudflare Workers. Turso now recommends Turso Sync instead: explicit push() and pull() calls for TypeScript, Python, and Go, without the filesystem dependency.
D1 keeps writes to a single primary region chosen at database creation. Global read replication entered public beta in April 2025. When enabled, D1 automatically provisions read-only replicas across all six supported regions with approximately 30–75ms replication lag. To use them, you opt in via the Sessions API:
const session = env.DB.withSession("first-primary");
const result = await session.prepare("SELECT * FROM users").all();
Without withSession(), all queries go to the primary regardless of where replicas exist. The Sessions API is only available through the Worker binding, not the REST API.
SDK and runtime portability
This is where the two products diverge most clearly.
Turso gives you three packages:
@tursodatabase/serverless— zero native dependencies, works anywherefetchis available (Node 12+, Deno, Cloudflare Workers, Vercel Edge, Netlify Edge)@libsql/client— the ORM integration package, Node.js natively, with@libsql/client/webfor edge runtimes@tursodatabase/database— local-first use backed by the new Turso Database engine, beta
You can query Turso from a Next.js API route on Vercel, a Node.js backend on AWS, a Deno Deploy function, or a Cloudflare Worker — same library, same SQL.
D1 is Workers-only. Access comes through an environment binding declared in wrangler.toml and injected at runtime:
const result = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(userId).all();
// Batch multiple statements in one round-trip
const [users, posts] = await env.DB.batch([
env.DB.prepare("SELECT * FROM users"),
env.DB.prepare("SELECT * FROM posts"),
]);
Cloudflare provides a REST API for external access, but their own documentation describes it as “best suited for administrative use” due to global API rate limits. For production external access, the recommended pattern is to build a proxy Worker — an HTTP layer that your external code calls. That is extra infrastructure on your critical path.
If any part of your stack lives outside Cloudflare Workers, D1 requires a proxy. Turso does not.
ORM support
Drizzle works well with both. For Turso, set the dialect to "turso" in your config and use @libsql/client:
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_URL!,
authToken: process.env.TURSO_TOKEN!,
});
export const db = drizzle(client);
For D1, pass the binding directly:
import { drizzle } from 'drizzle-orm/d1';
const db = drizzle(env.DB);
Drizzle Kit uses the d1-http driver for migrations against D1 via the REST API — a development-only path, not a production query path.
Prisma works with both, with caveats on each side.
For Turso (Prisma 5.4.2+): requires @prisma/adapter-libsql. Prisma Migrate and Introspection are not supported. The workaround is generating migrations locally against SQLite and applying them with turso db shell <db> < migration.sql.
For D1 (Prisma v5.12.0+, Preview status as of 2026): requires @prisma/adapter-d1. D1 has no interactive transactions, so Prisma’s $transaction() API is not supported. Prisma ORM 6.6.0 (April 2025) added D1 migrations in Early Access via Prisma’s own CLI commands: prisma db push (schema update), prisma db pull (introspection), and prisma migrate diff. Note: prisma migrate dev and prisma migrate deploy were explicitly not yet supported at that release.
In practice: if your team relies on Prisma Migrate as a first-class workflow, neither adapter delivers it cleanly. Drizzle’s migration story is smoother on both platforms. For a complete TypeScript ORM comparison that covers Drizzle, Kysely, Prisma, and TypeORM together, see Best TypeScript ORM 2026. If you are narrowing down between Drizzle and Kysely specifically, Drizzle vs Kysely covers the DX and migration trade-offs in depth.
Limits and quotas
| Limit | Turso Free | Turso Developer ($4.99/mo) | D1 Free | D1 Paid ($5/mo base) |
|---|---|---|---|---|
| Databases | 100 | Unlimited | 10 | 50,000 |
| Max DB size | Not published | Not published | 500 MB | 10 GB |
| Total storage | 5 GB | 9 GB | 5 GB | Up to 1 TB |
| Rows read | 500M/month | 2.5B/month | 5M/day | 25B/month |
| Rows written | 10M/month | 25M/month | 100K/day | 50M/month |
| Transactions | Yes | Yes | No BEGIN/COMMIT; batch() is atomic | No BEGIN/COMMIT; batch() is atomic |
| Queries/invocation | N/A | N/A | 50 | 1,000 |
| Over-limit behavior | BLOCKED error | BLOCKED error | Request fails | Per-million charges |
Turso does not publish per-database size limits; storage is metered at the account level across all databases at 4 KB page granularity. When you exceed any Turso limit, queries return a BLOCKED error code rather than queuing or degrading gracefully.
Real-world scenarios
Building a Cloudflare Workers SaaS. D1 is the default here. Zero connection strings, no separate infrastructure, pricing is straightforward. The Workers binding is tighter than any client library approach — you can’t misconfigure it because there is nothing to configure beyond wrangler.toml. The 10 GB per-database limit and 50,000 database cap make multi-tenant SaaS viable. The 50-queries-per-invocation free tier limit is the one thing to plan around early.
Building portable code not tied to Cloudflare. Turso. Your database access layer works in Node.js, Deno, Vercel, Netlify, or wherever. The libSQL client is standard SQL — no binding injection, no proxy Workers. When you need to run integration tests locally or migrate to a different host, you do not rebuild the data layer.
Mixed stack — Workers plus external services. This is the pain case for D1. Background jobs, webhook handlers, and data pipelines that run outside Workers need a proxy to reach D1. Turso’s @tursodatabase/serverless package works from both sides without ceremony.
Vector search. Turso has native F32_BLOB vector types and DiskANN indexing built into libSQL — no extension required. D1 has no native vector support. If you are building anything with embeddings, this is not close.
Choosing a managed database instead. If neither edge SQLite fits your use case — perhaps you need row-level security, multiple schemas, or managed Postgres — Supabase vs Firebase compares the two most popular managed database backends at 100K MAU.
Verdict
Pick D1 if your entire stack is on Cloudflare Workers. The pricing is generous at scale, global read replication is production-ready, and there is no separate database infrastructure to manage. The Workers binding model is simpler than any connection-string approach. What you give up is portability — D1 is a Cloudflare product, full stop.
Pick Turso if you need portability, transactions, or a database that works across runtimes. The free tier is more usable for individual developers (monthly limits rather than daily), and the open-source libSQL engine means the wire protocol and file format are not owned by a single vendor. If Turso’s business changes tomorrow, your data is still valid SQLite.
The decision is architectural more than technical. Committing to D1 is committing to Cloudflare. That is fine if you are committed anyway — D1 is a genuinely good product. If you are not, Turso is the one that will still work when you move.