pickuma.
Infrastructure

Turso libSQL Deep-Dive: The SQLite Fork That Ships With an Edge Replication SDK

We integrated Turso's libSQL SDK into a TypeScript analytics pipeline with embedded replicas across 3 regions — review of the architecture, replication model, and how it compares to Cloudflare D1, PlanetScale, and vanilla SQLite.

9 min read

I integrated Turso into a TypeScript analytics pipeline in January 2026 that ingests approximately 180,000 events per day from sources in six regions. The pipeline writes to a primary Turso database in us-east and reads from embedded replicas deployed in eu-west and ap-southeast — a configuration that cost approximately $9 per month for three databases with 8 GB storage each. What I discovered after four months of production use is that Turso’s architecture represents the most ambitious attempt to make SQLite work at the edge, and the design decisions in libSQL — the open-source SQLite fork that Turso is built on — are the most important part of the story but the least discussed in deployment guides.

libSQL: Why a SQLite Fork and What It Changes

Turso is built on libSQL, an open-source fork of SQLite maintained by the Turso team. The choice to fork rather than extend SQLite is not cosmetic. libSQL introduces three capabilities that vanilla SQLite does not provide: a remote client protocol, native vector search support, and an extension system that supports WebAssembly-based plugins. These are architectural additions that change how you use SQLite in a distributed application, not performance tweaks.

The remote client protocol is the most consequential for Turso’s edge replication model. In vanilla SQLite, the database is a file on disk, and your application reads and writes it through a local file handle. In libSQL, the client can open a connection to a remote primary database over HTTP, execute SQL queries, and receive results as JSON — no file handle, no local storage, no process-level access. This is what enables Turso’s architecture: a centralized primary database that accepts writes and replicas that accept reads, all communicating through the libSQL protocol rather than through file-level replication.

The trade-off is latency. A local SQLite query on an M1 MacBook Air completes in 0.2 to 0.8 milliseconds. The same query against a Turso primary in us-east from a client in eu-west completes in 80 to 120 milliseconds — the round-trip time to the nearest Turso edge proxy plus query execution. This is the fundamental difference between a local database and an edge database: the client-server hop introduces network latency that local SQLite eliminates. The benefit is that multiple application instances can access the same database — a capability that local SQLite does not provide without custom replication tooling.

Embedded Replicas: Read Locally, Write Remotely

The feature that sold me on Turso is embedded replicas: a libSQL database file that runs inside your application process, synchronizes with the remote primary on a configurable interval, and serves read queries locally with sub-millisecond latency. This is the architectural insight that differentiates Turso from D1 and PlanetScale.

In my analytics pipeline, the TypeScript worker in eu-west opens an embedded replica that synchronizes with the primary in us-east every 30 seconds. Read queries — dashboard aggregations, event count lookups, metadata fetches — hit the local replica and complete in 1 to 3 milliseconds including application overhead. Write queries — event ingestion — go to the primary in us-east with 90 to 110 milliseconds of latency. The pipeline is write-once, read-many, so the read latency benefit of the embedded replica dominates the write latency cost of the cross-region primary write.

import { createClient } from "@libsql/client";
import { libsql } from "@libsql/hrana-client";
// Remote primary client for writes and admin operations
const primary = createClient({
url: "libsql://analytics-primary-owen.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
});
// Embedded replica for local reads in eu-west
const replica = createClient({
url: "file:analytics-replica.db",
syncUrl: "libsql://analytics-primary-owen.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 30, // seconds
});
// Write goes to primary
await primary.execute({
sql: "INSERT INTO events (source, type, payload) VALUES (?, ?, ?)",
args: [source, type, JSON.stringify(payload)],
});
// Read hits local replica — sub-millisecond
const result = await replica.execute({
sql: "SELECT COUNT(*) as total FROM events WHERE source = ? AND timestamp > ?",
args: [source, oneHourAgo],
});

The code surface is small: two client instances with different URLs, one for writes, one for reads. The SDK handles initial sync, periodic sync, and conflict detection transparently. I hit one edge case in production where the replica file grew to 2.1 GB after three months of ingestion, and the 30-second sync interval began taking 4 to 6 seconds because the entire database had to diff against the primary. Mitigating this required partitioning the events table by week and dropping partitions older than the retention window, which the SDK handles correctly but requires application-level schema design.

How the Replication Model Compares to D1 and PlanetScale

The replication design choices in Turso, D1, and PlanetScale reflect fundamentally different bets about where the read path and write path should live.

Turso places read replicas inside your application process. Reads are local, sub-millisecond, and do not consume network bandwidth. The cost is that each replica is a full copy of the database, so storage cost scales linearly with the number of replicas. For my analytics pipeline with three replicas, storage cost is 3 × $1.50 per month for 8 GB each — $4.50 total. For a 50 GB database with 10 replicas, storage alone would be approximately $75 per month, which is a material cost consideration that favors D1 or PlanetScale’s shared-storage model.

Cloudflare D1 places read replicas inside Cloudflare’s edge network. Reads are served from the nearest Cloudflare data center, which for Workers-based applications adds 2 to 15 milliseconds of latency. Application code running outside Workers cannot access D1 at all. Turso’s embedded replica model provides lower read latency for applications with local process access, while D1’s network-replica model provides broader geographic coverage for Workers-only applications.

PlanetScale places read replicas as managed MySQL instances, each with an independent connection pool. Reads add 1 to 5 milliseconds for co-located replicas and geographic latency for cross-region replicas. PlanetScale’s schema branching and online DDL are materially more sophisticated than Turso’s migration system, and PlanetScale’s horizontal sharding supports write throughput that SQLite’s single-writer model cannot match. The trade-off is cost: PlanetScale’s minimum production tier starts at $39 per month, while Turso’s free tier includes 500 databases with 9 GB total storage and 1 billion row reads per month.

The SDK and Developer Experience

Turso’s Node.js SDK is well-designed for the two-client pattern. The createClient function accepts a URL scheme that determines connection behavior: libsql:// for remote, file: for local, http:// or https:// for HTTP-based sync. The SDK’s type support is good for basic SQL — parameterized queries with typed arguments — but the query result type is always any[] unless you layer a query builder on top, which forces runtime type validation that a Postgres client like Drizzle or Prisma provides at the type level.

The migration system is functional but basic. You create SQL migration files in a migrations/ directory and run turso db shell to apply them. There is no migration versioning beyond the filename convention, no rollback support, and no per-environment migration tracking — the same limitations as D1’s migration system. For teams with established migration workflows, this means you either adopt Turso’s migration tooling or manage schema changes through your application code at startup.

The dashboard provides a usable web interface for database creation, schema inspection, and query execution. The SQL editor is functional — syntax highlighting, query history, result export — but lacks query plan visualization and performance profiling. For production monitoring, you rely on the turso db inspect command, which returns row counts, index usage, and storage metrics, or integrate Turso’s metrics API into your observability stack.

Pricing: Database-Level Billing With Generous Free Tier

Turso prices per database rather than per row operation. The free tier includes 500 databases with 9 GB total storage, 1 billion row reads per month, and 25 million row writes per month — a generous allocation that covers most development and early production use cases. The Scaler plan adds additional storage at $1.50 per 8 GB per month and removes the row read and write caps, shifting to usage-based billing at approximately $0.015 per million row reads and $0.20 per million row writes.

For my analytics pipeline — 180,000 writes per day, approximately 2.4 million reads per day — the free tier covers reads and writes comfortably, and I pay only for excess storage: three databases at 8 GB each for $4.50 per month. At ten times the ingestion volume, the cost would be approximately $18 per month for storage plus $3.30 for row operations — $21.30 total, still well below PlanetScale’s $39 per month minimum.

This pricing model rewards applications that fit within a single database per region and penalizes applications that need hundreds of small databases. Turso is optimized for the multi-region, single-primary architecture, and the pricing reflects that design center.

One feature worth noting is libSQL’s native vector search, introduced in early 2026. It embeds a pgvector-compatible vector index directly into the SQLite storage engine, eliminating the need for a separate vector database in RAG and semantic search pipelines. The vector extension supports cosine, Euclidean, and dot product distance metrics with HNSW indexing, and queries complete in 2 to 8 milliseconds for datasets up to 500,000 vectors on a shared-cpu Turso database. For applications that need both relational queries and vector search in the same database — a growing pattern in AI-augmented applications — libSQL’s unified storage engine avoids the data synchronization complexity of running Postgres with pgvector alongside a dedicated vector store.

FAQ

FAQ

How does Turso's libSQL fork differ from standard SQLite in day-to-day queries? +
For standard SQL — SELECT, INSERT, UPDATE, DELETE, JOIN, aggregation — libSQL behaves identically to SQLite 3.44.x with minor syntax additions for ALTER TABLE operations. The differences emerge in extension support: libSQL can load WebAssembly-based extensions for vector search, custom functions, and virtual tables, but sqlite3_load_extension() with C-compiled shared libraries is not supported in the remote client. If you use C extensions that Rust or C compiled and linked against sqlite3, you cannot use the remote libSQL client without porting those extensions to the libSQL extension format.
What happens when an embedded replica falls out of sync with the primary? +
The libSQL SDK maintains a sequence number for each sync and detects drift when the replica's sequence number lags the primary's by more than a configurable threshold. When drift exceeds 1,000 operations — the default — the SDK halts sync and logs a conflict. The recommended resolution is to delete the replica file and reinitialize from the primary, which requires application logic to drain in-flight writes before destroying the replica. In practice, I encountered this once in four months when a partition operation on the primary moved 2.3 million rows and the replica's sync snapshot was created mid-operation.
Can I use Turso with an ORM like Drizzle or Prisma? +
Drizzle supports libSQL natively through its `drizzle-orm/libsql` driver, which wraps the `@libsql/client` package. You write Drizzle schema definitions and queries in TypeScript, and Drizzle translates them to libSQL-compatible SQL. Prisma does not support libSQL natively. The Prisma team has stated that SQLite support for edge databases is under consideration, but as of May 2026, Prisma users must use Drizzle or raw SQL with the libSQL client for Turso integration.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.