Cloud & Infrastructure · Database
Turso and LibSQL: SQLite at the Edge for Production Applications
SQLite runs in every phone and browser. Turso extends it to the server and edge with replication, branching, and a network protocol. Here's when it makes sense.
Anurag Verma
6 min read
Sponsored
SQLite is the most deployed database in history. It runs in your iPhone, in Chrome’s local storage implementation, in countless embedded systems, and in the servers of applications that found no reason to run a separate database process when a file would do. What it couldn’t do was scale across multiple servers or replicate reads to locations close to your users.
Turso, built on top of libSQL (a fork of SQLite that adds replication and a network protocol), changes that. It’s not the right tool for every use case, but for the cases it fits, the operational simplicity is notable.
What LibSQL Added to SQLite
LibSQL started as a fork of SQLite’s C source code with several additions:
- A client-server protocol (over HTTP and WebSocket) so SQLite can be accessed over a network, not just a file system
- Replication support for creating read replicas
- Write-ahead log streaming so replicas stay current
- Embedded replica mode: a local SQLite file that syncs from a remote primary, with reads served locally and writes forwarded to the primary
The fork tracks SQLite upstream. Existing SQLite queries work unchanged. The wire format is compatible enough that most SQLite drivers can speak to a libSQL server with minor configuration.
The Turso Model
Turso runs libSQL servers at the edge in 35+ regions. You create a database in a primary region and replicate it to additional regions. Reads hit the nearest replica. Writes go to the primary.
This architecture fits a specific pattern well: applications with a high read-to-write ratio, where reads are distributed globally and writes are less frequent. A blog, a product catalog, a user preferences system, a content API.
The embedded replica feature is where Turso gets interesting for edge functions. A Cloudflare Worker or Deno Deploy function can use a libSQL embedded replica: a local SQLite file that syncs from the remote primary. Reads are fully local, with sub-millisecond latency. The sync happens in the background.
Getting Started
Install the Turso CLI and create a database:
brew install tursodatabase/tap/turso
turso auth login
turso db create my-app
turso db show my-app
Get your connection URL and auth token:
turso db tokens create my-app
Connect from Node.js:
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// Standard SQLite syntax
const result = await client.execute(
'SELECT id, title, published_at FROM posts WHERE published = 1 ORDER BY published_at DESC LIMIT 20'
);
console.log(result.rows);
// [{ id: 1, title: 'Getting Started', published_at: '2026-05-01' }, ...]
Parameterized queries work as expected:
const post = await client.execute({
sql: 'SELECT * FROM posts WHERE slug = ?',
args: [slug],
});
if (post.rows.length === 0) {
return null;
}
return post.rows[0];
Transactions:
const tx = await client.transaction('write');
try {
await tx.execute({
sql: 'INSERT INTO comments (post_id, author, body) VALUES (?, ?, ?)',
args: [postId, author, body],
});
await tx.execute({
sql: 'UPDATE posts SET comment_count = comment_count + 1 WHERE id = ?',
args: [postId],
});
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}
The Embedded Replica Pattern
For edge deployments, the embedded replica is what makes Turso distinct from just hosting SQLite behind an HTTP API.
import { createClient } from '@libsql/client';
// Creates a local SQLite file that syncs from the remote primary
const client = createClient({
url: 'file:local.db', // local replica
syncUrl: process.env.TURSO_DATABASE_URL!, // remote primary
authToken: process.env.TURSO_AUTH_TOKEN!,
syncInterval: 60, // sync every 60 seconds
});
// Reads come from the local file — zero network latency
const posts = await client.execute('SELECT id, title FROM posts LIMIT 10');
// Writes go to the primary
await client.execute({
sql: 'INSERT INTO page_views (post_id, viewed_at) VALUES (?, ?)',
args: [postId, new Date().toISOString()],
});
// Force a sync when you need fresh data
await client.sync();
In a Cloudflare Worker, this local database lives in the Worker’s ephemeral storage. On each cold start, it syncs from the primary. On a warm instance, it reads from the cached file. The pattern works for content that can tolerate a small amount of staleness on edge reads.
Schema Management
Turso doesn’t have a built-in migration system. Most teams use Drizzle ORM, which has first-class libSQL support:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
slug: text('slug').notNull().unique(),
title: text('title').notNull(),
body: text('body').notNull(),
published: integer('published', { mode: 'boolean' }).default(false),
publishedAt: text('published_at'),
});
export const comments = sqliteTable('comments', {
id: integer('id').primaryKey({ autoIncrement: true }),
postId: integer('post_id').notNull().references(() => posts.id),
author: text('author').notNull(),
body: text('body').notNull(),
createdAt: text('created_at').notNull(),
});
Generate and apply migrations:
npx drizzle-kit generate
npx drizzle-kit migrate
Multi-Tenant Databases
Turso’s per-database pricing model makes it practical to create one database per customer — an approach that’s expensive with Postgres RDS but inexpensive with Turso. Customer data is isolated by default, backup and restore are per-customer, and a runaway query from one customer can’t affect another’s performance.
// Create a database per tenant on sign-up
const turso = createTursoClient({
token: process.env.TURSO_API_TOKEN!,
org: process.env.TURSO_ORG!,
});
async function createTenantDatabase(tenantId: string) {
const db = await turso.databases.create({
name: `tenant-${tenantId}`,
group: 'production',
});
const token = await turso.databases.createToken(db.name);
// Apply schema migrations to the new database
const client = createClient({ url: db.hostname, authToken: token.jwt });
await applyMigrations(client);
return { url: db.hostname, token: token.jwt };
}
When Not to Use Turso
Turso doesn’t fit every workload. The constraints to know before committing:
Heavy writes. SQLite’s write concurrency is single-writer-per-database. The primary handles all writes in sequence. For high-write workloads — event logging, real-time analytics, or anything where writes arrive in parallel — Postgres with connection pooling handles this better.
Large datasets. SQLite is a file, and a large file means longer sync times for replicas and embedded replica cold starts. For databases above a few gigabytes, the operational story becomes less clean.
Complex queries. SQLite’s query planner is good but not Postgres-good. Window functions, CTEs, and full-text search exist but with some limitations. If you’re planning to run complex analytical queries, Postgres is a better starting point.
For content APIs, user preference storage, small SaaS products, and multi-tenant applications with moderate per-tenant data sizes, Turso’s simplicity and edge-native design are genuine advantages. The combination of serverless pricing, no connection pooler to manage, and reads that happen at the database file level rather than over a network makes the architecture straightforward in a way that Postgres-on-a-VM isn’t.
Sponsored
More from this category
More from Cloud & Infrastructure
Cloudflare Workers AI: Running Models at the Edge Without a GPU Bill
Blue-Green and Canary Deployments: A Production Guide for Engineering Teams
eBPF in 2026: The Observability Superpower Hiding in Your Linux Kernel
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored