How Turso libSQL Embedded Replicas Bring P99 Read Latency Below 10ms in Serverless Functions
When I first encountered serverless DB latency issues, I reduced Redis TTL, tuned the connection pool, and cached JSON at the CDN. But the root cause was "the round trip to the DB itself," and caching only masked that round trip. Digging into why P99 latency was so erratic, you arrive at the fact that a remote DB round trip generating 30–150ms RTT per request is pulling the tail of the distribution.
First, some terminology. "Edge functions" means different things across platforms. Runtimes based on V8 Isolates with no filesystem — like Cloudflare Workers — are called edge functions, and so are Vercel Functions (Node.js runtime), AWS Lambda, and Fly.io VMs. The patterns covered in this post apply only to Node.js-family runtimes that have a filesystem. They do not work on Cloudflare Workers or Vercel Edge Runtime. This distinction is the foundational assumption for everything that follows.
Turso's Embedded Replica replicates a remote Turso database into a local SQLite file, and reads are served entirely from local disk — no network involved. Only writes are forwarded to the remote primary, and changes are synced in the background at the WAL frame level. Because it's a full SQLite file rather than a simple cache, even complex aggregate queries run locally without modification.
Core Concepts
What is libSQL
libSQL is an open-source fork of SQLite (MIT license). Turso created it to address SQLite's lack of distributed capabilities, and it is fully compatible with existing SQLite. Schemas, queries, indexes — all work as-is. On top of that, it adds remote replication, WAL-based sync, and HTTP/WebSocket access.
Latency Model: Reads and Writes Are Completely Different
The latency characteristics of reads and writes in embedded replicas are completely different. Confusing the two leads to wrong expectations.
| Operation | Path | Latency |
|---|---|---|
| Read | Local disk I/O only | Avg 624μs, P99 under 1ms (Turso official benchmark) |
| Write | Remote primary round trip required | Tens to hundreds of ms (varies by primary location) |
| Sync | Incremental WAL frame-level | Background, no full file retransmission |
Write latency is not improved at all. This technique is effective for workloads where read traffic is the overall DB bottleneck. It delivers the greatest benefit in read-heavy cases like marketing site product catalogs, dashboard aggregations, A/B test flag serving, and leaderboards.
So where does "under 10ms" in the title come from? 624μs is the latency of the DB query itself; the serverless function end-to-end P99 also includes runtime initialization and network stack overhead. The goal of this pattern is that once the DB round trip (30–150ms) is eliminated, the end-to-end P99 drops to the 10ms range.
WAL-Based Sync and Read-Your-Writes Consistency
WAL (Write-Ahead Log)-based sync streams only the delta. If 10 rows changed, only the WAL frames for those 10 rows are sent — not the entire GB-sized file.
Read-your-writes consistency is guaranteed through the sync() call. After a write is committed to the primary, calling sync() pulls those WAL frames down locally, allowing subsequent local reads to see your own writes. Automatic sync (syncInterval) runs periodically in the background, so if you need to read immediately after a write on the local replica, you need an explicit sync() call.
Design constraint in multi-instance deployments: In Vercel serverless or Fly.io multi-VM environments, each instance has its own separate local SQLite file. The point at which data written on instance A is reflected on instance B is B's next sync() cycle. If a read request is routed to a different instance than the one that handled the user's write request, the user may see stale data within the syncInterval window. For example, refreshing the leaderboard immediately after updating a game score and not seeing your score reflected yet is exactly this case. This is not a caveat — it's a trade-off that must be accepted at the architecture level.
Runtime Compatibility — Check This First
| Runtime | Embedded Replica Support | Notes |
|---|---|---|
| Node.js | ✅ | Full support |
| Bun | ✅ | Full support |
| Deno | ✅ | Requires --allow-write flag |
| Cloudflare Workers | ❌ | No filesystem |
| Vercel Edge Runtime | ❌ | No filesystem |
| Fly.io / Railway VM | ✅ | Use with persistent volume |
If you're on Vercel, you must deploy as a Node.js server function, not an Edge Runtime. In Next.js, remove the runtime: 'edge' setting and leave it as the default Node.js.
Practical Application
Basic Setup
npm install @libsql/clientimport { createClient } from "@libsql/client";
const client = createClient({
url: "file:./local.db", // Local SQLite file path
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60, // Unit: seconds. 60 = every 60 seconds
});
// Ensure latest state on app start
await client.sync();
// All subsequent reads are 100% from local disk
const result = await client.execute(
"SELECT * FROM products WHERE id = ?",
[productId]
);The unit for syncInterval is seconds. 60 means every 60 seconds, 5 means every 5 seconds. If you mistake it for milliseconds and set syncInterval: 5000, sync will occur roughly every 83 minutes. This is an easy mistake to make, so we'll revisit it later.
Scenario 1: Next.js API Route (Node.js Runtime)
// app/api/products/route.js
// Must NOT have runtime: 'edge' set — this runs on Node.js
import { createClient } from "@libsql/client";
let client;
async function getClient() {
if (!client) {
client = createClient({
url: "file:/tmp/local.db", // Temporary storage for Vercel serverless functions
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 30,
});
// Sync once on client creation to ensure fresh data
await client.sync();
}
return client;
}
export async function GET(request) {
const db = await getClient();
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const result = await db.execute(
"SELECT id, name, price FROM products WHERE category = ? ORDER BY name",
[category]
);
return Response.json(result.rows);
}When a Vercel serverless container restarts,
/tmp/local.dbis wiped. Becausesync()is called insidegetClient(), it automatically re-syncs on the first request after a restart. The pattern of creating the client only once at the module level prevents unnecessary syncs when the container is reused.
Scenario 2: Fly.io VM + Persistent Volume (Most Stable Pattern)
Mounting a persistent volume on Fly.io keeps the local DB file intact across redeployments. This lets you avoid the full sync cost on cold start.
# fly.toml
[mounts]
source = "db_volume"
destination = "/data"import { createClient } from "@libsql/client";
const client = createClient({
url: "file:/data/local.db", // Persistent volume path
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 10, // Unit: seconds
});
await client.sync();
console.log("Local replica synced, ready for reads");Scenario 3: Dashboard Aggregate API
Complex aggregate queries also run entirely locally. With no network latency, joins and subqueries benefit especially. A user-reported case showed P99 improvement from 45ms to 8ms compared to Cloudflare D1.
const stats = await client.execute(`
SELECT
date(created_at) as day,
COUNT(*) as order_count,
SUM(amount) as revenue,
AVG(amount) as avg_order
FROM orders
WHERE created_at >= date('now', '-30 days')
GROUP BY date(created_at)
ORDER BY day DESC
`);Scenario 4: Leaderboard (Freshness vs. Latency Trade-off)
For leaderboards where second-by-second updates aren't required, you can combine periodic sync with local reads.
const client = createClient({
url: "file:./leaderboard.db",
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 5, // Unit: seconds
});
// GET /api/leaderboard — local read, no network
// SQLite 3.25+ / libSQL supports: rank() OVER
const leaderboard = await client.execute(`
SELECT username, score, rank() OVER (ORDER BY score DESC) as rank
FROM scores
ORDER BY score DESC
LIMIT 100
`);
// POST /api/score — forwarded to remote primary
await client.execute(
"INSERT OR REPLACE INTO scores (user_id, score) VALUES (?, ?)",
[userId, newScore]
);This pattern is where multi-instance inconsistency becomes visible in practice. If a leaderboard request is served by a different instance immediately after a score update, the previous ranking will be visible for up to syncInterval (5 seconds here). Verify upfront whether this level of lag is acceptable for your service requirements.
New Turso Sync Protocol (@tursodatabase/sync)
A migration is underway from physical page-level replication to logical CDC (Change Data Capture). According to Turso's official benchmarks (2025), this is 7.3× faster than the previous approach (vendor self-measured, not independently verified), and the push()/pull() API also supports offline-first patterns.
import { createClient } from "@tursodatabase/sync";
const client = await createClient({
url: "file:./local.db",
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
// New API: explicit pull/push
await client.pull(); // Fetch remote changes
await client.push(); // Upload local changesSince this is currently in transition, it's worth verifying stability before adopting it in production.
Pros and Cons
Advantages of Embedded Replicas
| Item | Details |
|---|---|
| Read latency eliminated | Avg 624μs, P99 under 1ms (Turso official benchmark). Zero network round trips |
| Full SQLite compatibility | Existing queries and schemas work as-is, minimal migration cost |
| Incremental sync | Only WAL frame deltas are transferred, bandwidth-efficient |
| Read-your-writes | After calling sync(), local reads can see your own writes |
| No cache layer needed | Local reads at the DB level without Redis |
Constraints and Caveats
| Item | Details |
|---|---|
| Filesystem required | Cannot be used with CF Workers or Vercel Edge Runtime |
| No write latency improvement | All writes still require a remote primary round trip |
| Cold start cost | Initial full file download. Significant for multi-GB databases |
| Eventual consistency across replicas | Other instances may read stale data until next sync |
| Single primary bottleneck | Strong consistency for multi-region writes is not possible |
Comparison with Competing Technologies
The figures below are based on Turso's official comparison data and will vary by region, data size, and query complexity. They may differ from independent benchmarks.
| Technology | Read Latency | Filesystem Required | Write Strong Consistency | Notes |
|---|---|---|---|---|
| Turso Embedded Replica | Avg 624μs, P99 <1ms | ✅ Required | ❌ | Read-optimized |
| Cloudflare D1 | P99 ~8–15ms | ❌ Not required | ❌ | Workers native |
| Fly.io LiteFS | ~1–5ms | ✅ Required | ❌ | Fly platform dependent |
| PlanetScale / Neon | P99 20–100ms | ❌ Not required | ✅ | General-purpose serverless DB |
| Redis (Upstash) | <1ms | ❌ Not required | ✅ (simple KV) | No SQL |
Common Mistakes in Practice
1. Confusing syncInterval units
This is the most common mistake.
// Wrong — syncs every 5000 seconds (approx. 83 minutes)
syncInterval: 5000
// Correct — syncs every 5 seconds
syncInterval: 52. Directly accessing the local DB during sync
If another process opens the local SQLite file directly while a sync is in progress, data corruption can occur. Always access it exclusively through @libsql/client.
3. Applying this to write-heavy workloads incorrectly
This is not a fit for cases requiring many writes and immediate consistency, such as real-time chat, payment transactions, or inventory deduction. It's suited for workloads where read traffic is the overall DB bottleneck.
4. Attempting initial setup with a large database
Setting up for the first time with a multi-GB database can result in cold start times of tens of seconds to minutes. A practical upper limit is around a few hundred MB to 1GB.
5. Attempting to use it in Vercel Edge Runtime
Importing the libSQL client in middleware or a runtime: 'edge' API Route will cause a build error. Use it in Node.js runtime API Routes or Server Actions.
Decision Flow for When to Adopt
Closing Thoughts
Embedded replicas are a technology that replaces network round trips with disk I/O to bring DB read latency down to an average of 624μs and P99 of around 1ms. The core value is solving this at the DB level without a complex cache layer.
There are three constraints to verify before adopting.
It only works on runtimes with a filesystem (Node.js, Bun, Fly.io VM). If you're on Cloudflare Workers or Vercel Edge Runtime, you'll need a different approach.
Write latency is not improved. It delivers real benefits only for workloads where read traffic is the DB bottleneck.
You must accept an eventual consistency model across replicas. In a multi-instance environment, first confirm whether within-syncInterval inconsistency is acceptable for your service requirements.
If all three are acceptable, getting started is straightforward.
Step 1 — Confirm runtime: Verify that your current deployment environment is Node.js, Bun, or a filesystem-equipped VM. If you're on Cloudflare Workers or Vercel Edge Runtime, first consider switching to Cloudflare D1 or Node.js functions.
Step 2 — Confirm read bottleneck: Verify that read traffic is the primary cause of DB latency in your current workload. If writes are the bottleneck, a different approach than embedded replicas is needed.
Step 3 — Single endpoint pilot: Rather than migrating the entire DB at once, apply this to a single read-heavy API endpoint first to directly observe P99 improvement.
References
- Embedded Replicas Official Docs — Turso
- Microsecond-level SQL query latency with libSQL local replicas — Turso Blog
- Introducing Embedded Replicas: Deploy Turso anywhere — Glauber Costa (Medium)
- Turso Sync: a much, much, much better way to sync — Benchmark
- Introducing Databases Anywhere with Turso Sync
- tursodatabase/embedded-replica-examples — GitHub
- Turso Sync Engine Architecture — DeepWiki
- Turso on the Vercel Marketplace: Edge SQLite vs the Serverless Connection Pool
- Use Embedded Replicas of LibSQL with Golang — Meet Gor
- Distributed SQLite: Why LibSQL and Turso are the New Standard in 2026 — DEV Community
- Turso vs Cloudflare D1 for Solo Developers — SoloDevStack
- Post-PostgreSQL: Is SQLite on the Edge Production Ready? — SitePoint