website/src/content/posts/2025-02-16-sqlite-on-the-server-is-misunderstood/page.mdx
import SqliteMicroscale from "./sqlite-microscale.png"; import Cassandra from "./cassandra.png"; import SqliteHyperscale from "./sqlite-hyperscale.png";
<Lead> We're Rivet, a new open-source, self-hostable serverless platform. We've been in the weeds with SQLite-on-the-server recently and – boy – do we have a lot of thoughts to share. [Give us a star on GitHub](https://github.com/rivet-dev/engine), we'll be sharing a lot more about SQLite soon! </Lead>There's been a lot of discussion recently about the pros and cons of SQLite on the server. After reading many of these conversations, I realized that my perspective on the power of SQLite-on-the-server is lopsided from popular opinion: SQLite's strengths really shine at scale, instead of with small hobbyist deployments that it's frequently referenced in.
Before jumping in to my perspective on the benefits of SQLite at scale, it's helpful to understand some background on SQLite-on-the-server for micro-scale apps.
Most developers consider server-side SQLite a simple, cost-effective choice for small-scale applications. It's often valued for:
These characteristics make SQLite an attractive option for personal projects, lightweight applications, and prototypes.
Tools like LiteFS, Litestream, rqlite, Dqlite, and Bedrock enhance SQLite with replication and high availability for micro-scale deployments.
However, this post focuses on Cloudflare Durable Objects and Turso to highlight the often-overlooked advantages of SQLite at scale.
In high-scale systems, companies frequently struggle scaling databases like Postgres or MySQL. Instead, they often turn to sharded databases such as Cassandra, ScyllaDB, DynamoDB, Vitess (sharded MySQL), and Citus (sharded Postgres).
These systems use partitioning keys to co-locate related & similarly structured data. For example, a typical chat application on Cassandra might define:
CREATE TABLE chat_channel (
-- Partition Key: Groups all messages for a single chat in the same partition
channel_id UUID,
-- Clustering Key: Orders messages within the chat (think ORDER BY)
sent_at TIMESTAMP,
message_id UUID,
-- Row data
message TEXT,
PRIMARY KEY (channel_id, sent_at, message_id)
) WITH CLUSTERING ORDER BY (sent_at ASC, message_id ASC);
To query messages from this partition, you could write:
SELECT * FROM user_chat WHERE channel_id = ? ORDER BY sent_at ASC, message_id ASC;
Sharded databases power almost every large tech company because they provide:
While partitioning strategies improve scalability, they introduce significant challenges:
Cloudflare Durable Objects and Turso demonstrate how SQLite will change how hyper-scale applications may be architected in the future.
These databases provide:
Using SQLite with Cloudflare Durable Objects & Turso allows defining databases per entity, effectively replacing partitioning keys.
Instead of storing chat logs in a single partition, each chat channel can have its own SQLite database that also includes more tables, like participants and reactions. A sample schema could look like this:
-- Notice there is no channel_id in this table, since all of this data is local
-- to a single SQLite database per channel.
--
-- The channel ID will be specified when defining which Durable Object to
-- connect to below (see )`CHAT_DO.idFromName(channelId)` below.
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
sent_at TIMESTAMP,
sender_id UUID,
message TEXT
);
CREATE INDEX ON messages (sent_at ASC);
CREATE TABLE participants (
user_id UUID PRIMARY KEY,
joined_at TIMESTAMP
);
From Cloudflare Durable Objects or Turso, this SQLite partition database could be queried like this:
<CodeGroup> ```javascript {{ "title": "cloudflare_worker.js" }} // This Durable Object represents one partition // // It contains a completely isolated SQLite database export class ChatChannel extends DurableObject { async getMessages() { // Execute query let result = this.ctx.storage.sql.exec('SELECT * FROM messages ORDER BY sent_at ASC').one(); return result; } }function getMessages(channelId: string) { // Here, the channel ID corresponds to the database partition const id = env.CHAT_CHANNEL_DURABLE_OBJECT.idFromName(channelId); const channelDurableObject = c.env.CHAT_CHANNEL_DURABLE_OBJECT.get(id);
// Execute the query return await channelDurableObject.getMessages(); }
```javascript {{ "title": "turso.js" }}
import { createLibsqlClient } from "@libsql/client";
import { createApiClient } from "@tursodatabase/api";
const api = createApiClient(/* ... */);
function getMessages(channelId: string) {
// Each database represents one partition
//
// This gets an existing channel and connects to it
const { hostname } = await api.databases.retrieve(`channel-${channelId}`);
const db = createLibsqlClient({
url: `libsql://${hostname}`,
authToken: "..."
});
// Execute query
const result = await turso.execute("SELECT * FROM messages ORDER BY sent_at ASC");
return result.rows;
}
Despite its benefits, SQLite at scale presents a few challenges:
SQLite on the server is more than a lightweight solution for small deployments – it's an increasingly viable alternative to traditional partitioned databases. By leveraging SQLite-per-partition solutions like Turso and Durable Objects, developers gain rich SQL capabilities, ACID compliance, and significant operational advantages.