Edge-native DB stack with Turso + libSQL + Drizzle ORM — achieving single-digit millisecond reads on Cloudflare Workers
The first bottleneck I encountered when moving to serverless was DB latency. Computing had already been deployed to edge locations worldwide, but the database was pinned to a single region — us-east-1 in Virginia. When a user in Seoul called the API, a Workers instance spun up nearby, but the DB query made a round trip to the East Coast of North America. p99 latency climbed above 300ms in some windows, and edge computing was rendered powerless in the face of DB round-trip time.
When I first came across Turso, I had good reason to be skeptical. SQLite is a DB embedded in a single file — it seemed fundamentally at odds with distributed replication. Unlike client-server databases, SQLite works by having a single process read a file directly, and adding network replication requires redesigning the protocol from scratch. libSQL actually did that, and Turso layered a global replica infrastructure on top of it.
This post covers how Turso and libSQL work, how to build a type-safe query layer with Drizzle ORM, and the process of deploying to Cloudflare Workers.
libSQL: An Open-Source Fork of SQLite
libSQL is a public fork of SQLite. SQLite itself is in the public domain — about as permissive as licensing gets — but the project follows a closed development model. Code from outside the core team is not accepted, and there is no official CLA process for external contributors. The issue is not licensing; it is contribution policy. libSQL starts from that limitation and adds the following features:
- Remote replication and HTTP protocol support
- Additional
ALTER TABLEsyntax - DiskANN-based ANN (Approximate Nearest Neighbor) vector indexes
- WASM and edge-runtime-friendly builds
Binary-level compatibility with SQLite is maintained, so existing SQLite schemas and drivers can be carried over largely as-is.
Turso's Write-Read Separation Architecture
Turso is an edge DBaaS built on libSQL. The core design is writes go to a single Primary; reads go to the nearest replica.
The implementation details of Turso's internal routing (whether DNS-based or driver-level) are not specified in publicly available documentation. The diagram above is a simplified model of the operational behavior.
According to the Turso official documentation, replicas are deployed in 35+ locations (subject to change over time), and read requests are automatically routed to the physically nearest replica. The official documentation states that read latency below 10ms is achievable from edge replicas, though actual latency varies depending on network path, query complexity, and regional infrastructure conditions.
Writes must always go through the single Primary. Write serialization is guaranteed, but the Primary can become a bottleneck under high-write workloads. This tradeoff is revisited in the stack suitability matrix below.
Embedded Replica: A Feature Unavailable in Workers
Turso has a feature called Embedded Replica. It keeps a local SQLite file inside the application process and automatically synchronizes it with the Primary. Reads are served from this local file, so there is no network round trip. The Turso Sync benchmark blog (measured under conditions of in-process file reads and simple SELECT queries) reported average read latency around 624μs. Results will differ under different measurement conditions.
This feature cannot be used in Cloudflare Workers. The Workers runtime has no local filesystem, so a SQLite file cannot be created at all. Embedded Replica is only available in runtimes with filesystem access — Node.js servers, Tauri desktop apps, and Deno environments.
Drizzle ORM: A TypeScript Type-Safe Layer
Drizzle ORM is an ORM where declaring a schema in TypeScript automatically infers the types. It connects to libSQL-compatible clients via the drizzle-orm/libsql adapter, and migration files can be auto-generated and applied to a Turso DB using drizzle-kit.
Choosing a Driver per Runtime
Before writing any code, you must choose the right driver for your runtime. Picking the wrong one will cause a build failure or unpredictable runtime errors.
| Runtime | Driver | Notes |
|---|---|---|
| Node.js, Deno, bundler environments | @libsql/client |
Can use Node.js native modules; supports Embedded Replica |
| Cloudflare Workers, Pages | @tursodatabase/serverless |
Pure fetch() API-based; no filesystem required |
Using @libsql/client in a Workers environment will cause the build to fail due to Node.js native module dependencies. @tursodatabase/serverless is a pure HTTP driver that uses only the fetch() API and works in all edge runtimes.
To clarify the connection relationship: the object returned by createClient() from @tursodatabase/serverless follows a client interface that implements the libSQL HTTP protocol. The drizzle() function from drizzle-orm/libsql accepts this interface directly as an argument, so the two packages can be wired together without any additional adapter. That said, API signatures may change across major versions, so it is recommended to verify compatibility for your current versions in each package's official documentation.
Putting It Into Practice
Version note: The code examples in this post were written as of July 2026.
drizzle-orm,drizzle-kit,@tursodatabase/serverless, andhonoall have API changes across major versions, so check against the official documentation for the version you are using.
Creating a DB with the Turso CLI
# Install the Turso CLI (macOS)
brew install tursodatabase/tap/turso
# Log in
turso auth login
# List available regions
turso db locations
# If no region is specified, one is chosen automatically based on your current location
turso db create my-app-db
# Use the --location flag to specify a Primary location directly
# Refer to the output of `turso db locations` for exact region codes
turso db create my-app-db --location nrt
# Check the connection URL and token
turso db show my-app-db --url
turso db tokens create my-app-dbPrimary region selection directly affects write latency. If your users are primarily in Asia, placing the Primary in Tokyo or Singapore minimizes write latency. Reads are always served from the nearest replica regardless of where the Primary is, so region selection has a smaller impact on read performance.
Install packages in your Workers project:
npm install drizzle-orm @tursodatabase/serverless hono zod
npm install -D drizzle-kit wranglerDefining the Schema
// src/db/schema.ts
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const usersTable = sqliteTable('users', {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
email: text().notNull().unique(),
// SQLite has no datetime type; stored as a Unix timestamp (integer); Drizzle handles Date conversion automatically
createdAt: int({ mode: 'timestamp' }).$defaultFn(() => new Date()),
});
export const postsTable = sqliteTable('posts', {
id: int().primaryKey({ autoIncrement: true }),
title: text().notNull(),
content: text(),
authorId: int().notNull().references(() => usersTable.id),
});Type helpers like int and text automatically infer column types as TypeScript types. There is no need to cast query results separately.
Configuring drizzle.config.ts
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './migrations',
dialect: 'turso', // Must be 'turso', not 'sqlite'
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});# Generate migration files from schema changes
npx drizzle-kit generate
# Apply migrations to the Turso DB
npx drizzle-kit migrateBuilding an API with Cloudflare Workers + Hono
The key in the actual request flow is who decides routing. Reads are handled by the Seoul replica, and writes are automatically forwarded to the Primary internally by the Turso SDK. Application code does not need to know the Primary URL separately.
The client creation strategy in Workers also matters. Calling getDb() multiple times within a single request handler creates duplicate clients. Creating it once in a Hono middleware and injecting it into the context avoids this problem.
// src/index.ts
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@tursodatabase/serverless';
import { usersTable } from './db/schema';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
type Env = {
TURSO_DATABASE_URL: string;
TURSO_AUTH_TOKEN: string;
};
function getDb(env: Env) {
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
});
return drizzle(client);
}
type Variables = {
db: ReturnType<typeof getDb>;
};
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
app.use('*', async (c, next) => {
c.set('db', getDb(c.env));
await next();
});
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.get('/api/users', async (c) => {
const db = c.get('db');
const users = await db.select().from(usersTable);
return c.json(users);
});
app.get('/api/users/:id', async (c) => {
const db = c.get('db');
const rawId = Number(c.req.param('id'));
if (!Number.isInteger(rawId) || rawId <= 0) {
return c.json({ error: 'Invalid id' }, 400);
}
const user = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, rawId))
.get();
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user);
});
app.post('/api/users', async (c) => {
const db = c.get('db');
const parsed = userSchema.safeParse(await c.req.json());
if (!parsed.success) {
return c.json({ error: parsed.error.flatten() }, 400);
}
const result = await db.insert(usersTable).values(parsed.data).returning();
return c.json(result[0], 201);
});
export default app;When using c.set() and c.get(), the Variables type must be declared in the Hono generic. See the Context section of the Hono official documentation for a complete example of how to declare types.
Configuring wrangler.toml and Deploying
# wrangler.toml
name = "my-edge-api"
main = "src/index.ts"
# This date directly affects Workers runtime behavior.
# Check the Cloudflare Workers official documentation for the latest recommended date.
compatibility_date = "2026-07-01"Inject secrets via the Wrangler CLI rather than hardcoding them in source. After running each command, the terminal will wait for a value. Enter the value and press Enter to save it.
wrangler secret put TURSO_DATABASE_URL
# Enter the URL value at the prompt and press Enter
wrangler secret put TURSO_AUTH_TOKEN
# Enter the token value at the prompt and press EnterConnecting Turso from the Cloudflare Integrations Marketplace can automate part of this process with a few dashboard clicks. Deployment is a single line:
wrangler deployWhat You Should Know Before Choosing This Stack
Why DB-per-Tenant Is Practical in Multi-Tenant SaaS
One scenario where Turso particularly shines is multi-tenant SaaS. Running a separate DB per tenant in PostgreSQL requires fixed server resources per tenant. Because libSQL is one file per one DB, idle tenants consume virtually no resources. Issuing each tenant a separate URL and token enables both data isolation and independent scaling at once. See the Turso Database Per Tenant official documentation for architecture examples.
Local-First Apps Combined with Embedded Replica
Using Embedded Replica, environments with a filesystem — such as Tauri desktop apps or Node.js servers — can support offline reads while automatically syncing with the cloud. This feature is unavailable in Workers, but the strength shows in a monorepo structure where the same schema and Drizzle query code is shared between a desktop app and a server. For an example of applying Drizzle + Turso Sync to Tauri, see this DEV Community post.
Vector Search: In the Same DB as Relational Data
libSQL has a built-in DiskANN-based ANN index. Being able to manage vector embeddings and relational data in the same DB reduces initial stack complexity. That said, if you have large-scale vector search requirements, it is worth benchmarking against a dedicated vector DB. See the Turso official documentation for the API reference.
Choose Something Else for High-Write Workloads
The single-Primary model guarantees write serialization but has an upper bound on throughput. For scenarios requiring real-time event processing or hundreds of writes per second, Turso alone is not the right fit. Consider splitting writes to a PostgreSQL-family DB (Neon, Supabase, etc.) or an architecture that processes writes asynchronously via a queue.
Turso follows an eventual consistency model. A read request immediately after a write may not reflect the change. This stack is not appropriate for services where strong consistency is a business requirement.
Stack Suitability Matrix
| Item | Rating | Notes |
|---|---|---|
| Read latency | ✅ Excellent | Sub-10ms from edge replicas (per Turso official docs; varies by conditions) |
| SQLite compatibility | ✅ Excellent | Existing SQLite schemas and drivers can be reused |
| Serverless friendliness | ✅ Excellent | HTTP driver supports Workers, Deno Deploy, and Vercel Edge |
| Multi-tenant economics | ✅ Excellent | Minimal idle resource usage with a DB-per-tenant pattern |
| Built-in vector search | ✅ Excellent | Relational and vector data managed in a single DB |
| Drizzle ORM integration | ✅ Excellent | Type-safe pipeline from schema through migrations to queries |
| Write performance | ⚠️ Limited | Single-Primary model; not suitable for high-write workloads |
| Write consistency | ⚠️ Limited | Eventual consistency model; no strong consistency guarantee |
| Advanced SQL features | ⚠️ Limited | PostgreSQL-specific extensions like PostGIS and pg_trgm unavailable |
| Enterprise SLA | ⚠️ Caution | Startup-scale company; SLA terms warrant careful review |
| libSQL fork risk | ⚠️ Caution | Long-term compatibility with SQLite mainline requires ongoing monitoring |
| Replication engine stability | ⚠️ Caution | Cloud-native replication engine is in BETA as of July 2026 (check official docs for current status) |
Common Pitfalls in Practice
Attempting to Use Embedded Replica in Workers
The Workers runtime has no filesystem, so a local SQLite file cannot be created. Dropping in an Embedded Replica example as-is will result in a build error.
// ❌ Does not work in Workers
import { createClient } from '@libsql/client';
const client = createClient({
url: 'file:local.db',
syncUrl: env.TURSO_DATABASE_URL,
});
// ✅ Correct approach in Workers
import { createClient } from '@tursodatabase/serverless';
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
});Setting dialect to 'sqlite'
When targeting Turso in drizzle.config.ts, you must use dialect: 'turso'. Setting it to 'sqlite' can cause migrations to behave unexpectedly.
Creating a DB Client at Module Level
In Workers, the env object cannot be accessed outside of request context. Client creation must always happen inside a request handler or middleware.
Closing Thoughts
The services this stack suits well are clear: those with a high read ratio, users distributed across the globe, and data models simple enough to fit SQLite. Global APIs, multi-tenant SaaS, and local-first apps are the prime examples.
The services it does not suit are equally clear. Before choosing this stack for services that require strong consistency, high write throughput, or dependency on PostgreSQL-specific extensions, you should thoroughly evaluate the costs and limitations.
If you determine that this stack fits your service, there are natural directions to explore next: implementing microsecond reads with Embedded Replica in a Node.js environment, a multi-tenant branching pattern that switches per-tenant DB URLs at runtime, and implementing semantic search by combining libSQL vector indexes with Drizzle queries — these are the directions worth exploring to go deeper with this stack.
References
- Turso Official Documentation
- libSQL Official Documentation
- Introduction to Turso Embedded Replicas
- Drizzle ORM + Turso Official Tutorial
- Drizzle ORM + Turso Connection Guide
- Official Tutorial: Connecting to Turso from Cloudflare Workers
- Cloudflare Workers Third-party Integrations: Turso
- Turso Serverless JavaScript Driver Announcement Blog
- Turso Sync Benchmark Blog
- Turso Database Per Tenant Architecture
- Distributed SQLite: Why LibSQL and Turso are the New Standard in 2026 — DEV Community
- Turso Complete Guide 2026 (Oflight Inc.)
- The Edge-Native Stack: Cloudflare Workers and Turso
- Building a Local-First Tauri App with Drizzle ORM and Turso Sync — DEV Community
- Cloudflare Integrations Marketplace: Turso Partner Announcement