Skip to content

Split View: SQLite 르네상스 2026 — 24살 단일 파일 DB가 어떻게 가장 핫한 인프라가 되었나 (libSQL·Turso·LiteFS·D1·mvSQLite 심층 분석)

|

SQLite 르네상스 2026 — 24살 단일 파일 DB가 어떻게 가장 핫한 인프라가 되었나 (libSQL·Turso·LiteFS·D1·mvSQLite 심층 분석)

프롤로그 — 24살 데이터베이스가 왜 다시 핫해졌나

2026년의 인프라 스택을 보면 묘한 장면이 펼쳐진다. 한쪽에서는 Postgres 18이 멀티마스터 논리 복제를 다듬고, 다른 한쪽에서는 분산 NewSQL이 글로벌 일관성을 약속한다. 그런데 가장 시끄러운 트렌드는 그 어느 쪽도 아니다 — 2000년 D. Richard Hipp이 한 파일짜리 임베디드 라이브러리로 시작한 SQLite가 다시 무대 한복판에 섰다.

SQLite는 데이터베이스가 아니다. 라이브러리다. 그게 이 모든 부활의 비밀이다.

이상하게 들리지만 다시 생각해 보자. 2026년 인프라의 두 가지 거시적 압력은 (a) 엣지로 가는 컴퓨트(b) 로컬 우선 UX다. 둘 다 답이 같다 — "네트워크 라운드트립을 없애라". 그리고 네트워크를 없애려면 데이터가 코드 옆에 있어야 한다. 코드 옆에 있는 데이터베이스는 무엇인가? 로컬 파일. SQLite는 처음부터 로컬 파일이었다.

이 글은 24살 먹은 단일 파일 DB가 어떻게 2026년의 가장 흥미로운 인프라 카테고리로 부활했는지 해부한다. 정확히 무엇이 새로운지 — libSQL 포크와 Turso의 임베디드 레플리카, Cloudflare D1, 이제는 일몰된 LiteFS, FoundationDB 기반 mvSQLite, Litestream 백업 — 그리고 SQLite를 "엣지 데이터베이스"로 만든 토폴로지가 무엇인지. 그리고 정직하게 말한다 — SQLite가 옳은 선택일 때와 절대 틀린 선택일 때.


1장 · SQLite-at-the-edge 테제 — 왜 지금인가

먼저 명확히 하자. SQLite는 새롭지 않다. 1.0이 2000년에 나왔다. 모든 안드로이드, iOS, macOS, 모든 브라우저, 거의 모든 IoT 기기에 들어 있다. 세계에서 가장 많이 배포된 데이터베이스다. 그런데 왜 2026년에 갑자기 "트렌드"가 되었나?

세 가지가 동시에 일어났다.

첫째, 엣지 컴퓨트가 메인스트림이 됐다. Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Fly.io — 코드는 사용자 가까이로 갔다. 그런데 데이터베이스는? 여전히 us-east-1에 있다. 서울 사용자가 도쿄 엣지에서 실행되는 함수를 호출했는데, 그 함수가 us-east-1의 Postgres로 200ms 라운드트립을 하면, 엣지의 의미가 사라진다.

둘째, 로컬 우선(local-first) 운동이 형태를 갖췄다. Ink & Switch의 2019년 에세이로 시작된 흐름이 2026년에는 실제 제품들 — Linear, Figma, Notion의 일부 — 로 구체화됐다. "오프라인에서도 동작하고, 즉각적이고, 내 데이터는 내 기기에" 라는 약속. 이를 위해서는 데이터가 클라이언트 안에 있어야 한다. 클라이언트에 들어가는 임베디드 SQL DB는? SQLite다.

셋째, "스케일 아웃이 답"이라는 신화가 흔들렸다. 2020년대 초반에는 모든 회사가 "분산 분산 분산"을 외쳤지만, 2026년에 모두가 깨달은 것은 — 대부분의 앱은 분산 일관성이 필요 없다. 하나의 노드에 다 들어간다. 그리고 단일 노드 데이터베이스는 운영이 거짓말처럼 단순하다.

이 세 가지의 교집합이 바로 SQLite다. 로컬 파일이고, 임베디드이고, 작고, 신뢰할 수 있다. 거기에 2020년대 중반의 새 도구들이 SQLite의 약점 — 분산, 복제, 백업 — 을 메우기 시작한 것이 이 르네상스의 본질이다.


2장 · libSQL과 Turso — "임베디드 레플리카"라는 발명

이 부활의 가장 영향력 있는 한 가지를 골라야 한다면 Turso의 임베디드 레플리카(embedded replica) 다.

libSQL — SQLite의 오픈 포크

SQLite 자체는 오픈소스지만 외부 컨트리뷰션을 받지 않는다. SQLite 팀은 "퍼블릭 도메인이지만 우리 코드는 우리가 쓴다"는 입장이다. 이게 갈증을 만들었다 — 사람들은 SQLite에 새 기능 (스토리지 백엔드 교체, 네이티브 복제, 더 나은 동시성) 을 붙이고 싶었지만 길이 없었다.

그래서 2022~2023년경 Turso(당시 ChiselStrike)가 libSQL을 만들었다. SQLite의 진짜 포크다. MIT 라이선스, 외부 컨트리뷰션 받음, 그리고 가장 중요하게 — 네이티브 네트워크 프로토콜(HTTP, WebSocket via "Hrana"), 사용자 정의 함수, 그리고 임베디드 레플리카를 추가했다.

임베디드 레플리카 — 모델 자체가 새롭다

전통적인 모델: 앱 ↔ 네트워크 ↔ DB. 모든 쿼리가 라운드트립.

Turso의 임베디드 레플리카: 앱 안에 SQLite 파일이 있다. 읽기는 그 로컬 파일에서 일어난다 — 마이크로초 단위. 쓰기는 원격 마스터로 보내지고, 마스터가 변경분(WAL 프레임)을 백그라운드로 푸시해 로컬 파일을 동기화한다.

┌────────────────────────┐                    ┌──────────────────┐
│       앱 (예: 엣지)    │                    │   Turso 마스터   │
│  ┌──────────────────┐  │   읽기: 로컬 디스크 │  (글로벌 1차)    │
│  │ libSQL 클라이언트 │──┘   < 1ms          │                  │
│  │  + 로컬 .db 파일  │                       │                  │
│  └────────┬─────────┘                       │                  │
│           │ 쓰기 (HTTP/Hrana) ─────────────▶│                  │
│           │ ◀── WAL 프레임 (백그라운드 sync) │                  │
└────────────────────────┘                    └──────────────────┘

이게 뭐가 그렇게 새로운가? 읽기와 쓰기의 비대칭성을 처음으로 정직하게 받아들였다. 대부분의 웹 앱은 99% 읽기다. 읽기를 1ms 이하로 만들 수 있다면, 쓰기는 라운드트립을 감수해도 된다. 그런데 기존의 "리드 레플리카" 모델은 항상 네트워크 너머에 있었다. Turso의 임베디드 레플리카는 그 레플리카를 앱 프로세스 안의 디스크 파일로 가져왔다.

코드: Turso 임베디드 레플리카 셋업

// pnpm add @libsql/client
import { createClient } from '@libsql/client'

const db = createClient({
  // 로컬 SQLite 파일 (디스크에 진짜로 존재함)
  url: 'file:local.db',
  // 원격 1차 — 쓰기는 여기로
  syncUrl: 'libsql://my-db-myteam.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN,
  // 백그라운드 동기화 주기
  syncInterval: 60, // 초
})

// 첫 sync — 원격 상태를 로컬에 복제
await db.sync()

// 읽기 — 로컬 파일에서 곧장. 네트워크 없음.
const rows = await db.execute('SELECT * FROM posts WHERE published = 1')

// 쓰기 — 원격으로 가고, 다음 sync에 로컬도 따라잡음
await db.execute({
  sql: 'INSERT INTO posts (title, body) VALUES (?, ?)',
  args: ['Hello', 'World'],
})

// 수동 동기화도 가능
await db.sync()

이 모델의 의미는 큰데, 가장 큰 것 — 읽기는 로컬 파일 IO만큼 빠르다. SSD에서 SQLite 인덱스 룩업은 마이크로초 단위다. 어떤 분산 DB의 어떤 캐시도 이걸 못 이긴다. 트레이드오프는 일관성 이다. 로컬 레플리카는 마지막 sync 시점까지의 데이터만 본다. 쓰기 직후의 자기 데이터를 즉시 봐야 한다면 read-your-writes 모드를 켜야 하고 (그 경우 그 한 쿼리만 원격으로 가서 라운드트립 발생), 강한 일관성이 필요한 워크로드는 그냥 원격 모드를 써야 한다.

Turso 플랫폼 자체

Turso는 그 위에 매니지드 서비스를 얹었다. 무료 티어가 후하고 (수억 행 무료), 데이터베이스 브랜치 가 있고 (Git 브랜치처럼 DB를 분기), 멀티 리전 1차도 가능하다. 2024년 이후로 Turso의 큰 베팅은 per-tenant 데이터베이스 다 — 사용자 한 명당 DB 한 개. SQLite는 가볍기 때문에 수십만 개의 작은 DB가 가능하다. 멀티테넌시의 새 모델이다.


3장 · LiteFS — Fly의 분산 SQLite 실험과 그 일몰

Fly.io는 2022년에 다른 답을 시도했다 — LiteFS, FUSE 파일시스템 레이어다.

LiteFS의 아이디어

평범한 SQLite 파일을 FUSE 마운트 위에 두면, LiteFS가 그 파일에 일어나는 모든 트랜잭션(WAL 프레임)을 가로채서 다른 노드에 복제한다. 앱은 자기가 분산 환경에 있는지조차 모른다 — 그냥 SQLite 파일을 연다. 1개 노드가 "1차"이고 모든 쓰기는 1차로 라우팅된다(LiteFS Proxy가 자동으로 처리). 다른 노드들은 거의 실시간으로 복제본을 받는다.

가장 큰 매력은 앱 코드 변경이 없다는 점 이었다. Rails, Django, Phoenix — 어떤 프레임워크든 SQLite를 열 수만 있으면 LiteFS 위에서 분산 모드로 동작했다.

일몰

그런데 2024년 중반, Fly는 LiteFS 개발이 사실상 일시 정지 됐다고 공지했고, 2025년에는 새 사용자에게 더 이상 권장하지 않는다는 신호가 강해졌다(공식 "유지보수 모드(maintenance mode)" 라는 표현이 자주 인용된다). 기술적으로는 FUSE 자체의 복잡성, 멀티 1차의 어려움, 그리고 운영 부담 이 원인으로 거론됐다. Fly의 매니지드 SQLite 비전은 다른 방향(예: Fly Postgres 강화, Tigris 같은 외부 파트너십)으로 이동했다.

그래도 배울 점

LiteFS의 일몰이 아이디어의 실패는 아니다. LiteFS는 "SQLite를 어떻게 분산시킬 것인가" 에 대한 가장 깔끔한 답 중 하나였다. 앱 투명성, 단방향 1차, WAL-레벨 복제. 비록 매니지드 형태로는 살아남지 못했지만, libSQL과 D1이 그 디자인 어휘 — 1차 + 비동기 복제, WAL 프레임 단위 동기화 — 의 일부를 이어받았다.

오늘 분산 SQLite를 새로 시작한다면 LiteFS를 권하지 않는다. 그러나 그 디자인 문서들은 여전히 읽을 가치가 있다.


4장 · Cloudflare D1 — 엣지 SQLite의 매니지드 답

Cloudflare가 던진 답은 다르다. D1은 SQLite 자체를 Workers 런타임 근처에 가져다 놓는다.

D1의 모델

  • 저장 단위: SQLite 데이터베이스.
  • 호스팅: Cloudflare의 글로벌 네트워크 위, 일종의 1차 리전 + 자동 리드 레플리카로 동작.
  • 접근: Workers 바인딩 (env.DB.prepare(...))을 통해서. HTTP API도 있지만 주된 모델은 Workers 내부에서의 호출.
  • 상태: 2026년 기준으로 GA 상태이며, "Global Read Replication" 옵션을 통해 읽기 레플리카가 여러 대륙에 자동 배치된다. 데이터베이스당 크기 한도가 한 자릿수 GB(현재 10GB대) 수준이라 거대 단일 DB보다는 per-application 혹은 per-tenant 모델에 맞는다.

코드 예시: Workers + D1

// wrangler.toml에 D1 바인딩이 있다고 가정
// [[d1_databases]]
// binding = "DB"
// database_name = "blog"
// database_id = "..."

export interface Env {
  DB: D1Database
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url)
    const slug = url.pathname.slice(1)

    // prepare + bind — SQL injection 안전
    const stmt = env.DB.prepare(
      'SELECT title, body, published_at FROM posts WHERE slug = ?1 LIMIT 1'
    ).bind(slug)

    const row = await stmt.first<{ title: string; body: string; published_at: string }>()

    if (!row) return new Response('Not found', { status: 404 })

    return Response.json(row)
  },
}

핵심은 env.DB가 네트워크 핸들이 아니라 런타임이 주입한 바인딩 이라는 점이다. Workers는 D1과 같은 인프라 위에서 돌고, Cloudflare가 라우팅·풀링·복제를 다 숨긴다. 개발자 입장에서는 그냥 "엣지 함수 안에 SQL이 있다".

트레이드오프

  • 쓰기 일관성: 단일 1차에 직렬화. 다중 리전 쓰기는 없다. 글로벌 일관성을 약속하지 않는다.
  • 트랜잭션: 인터랙티브 트랜잭션은 제한적. 배치 API 가 권장된다 — 여러 statement를 한 번에 묶어 보낸다.
  • 크기: DB 1개당 한 자릿수 GB. 더 큰 데이터는 R2 + D1 메타데이터 패턴으로 분할.
  • lock-in: D1은 Workers 생태계 내부에서 가장 빛난다. 다른 곳에서는 매력이 줄어든다.

D1의 진짜 가치는 "Workers를 쓴다면 거의 마찰 없이 영속성을 얻는다" 는 데 있다. SQLite-at-the-edge의 가장 통합적인 구현이다.


5장 · 분산 SQLite의 다른 길 — mvSQLite와 rqlite

위의 셋(libSQL/Turso, LiteFS, D1)이 메인스트림이지만, 더 흥미로운 실험들이 있다.

mvSQLite — FoundationDB로 백킹

mvSQLite는 SQLite의 스토리지 엔진(VFS 레이어)을 통째로 갈아끼웠다. 데이터는 로컬 디스크가 아니라 FoundationDB 에 저장된다. FoundationDB는 Apple이 쓰는 분산 트랜잭셔널 키밸류 스토어로, 직렬화 가능 트랜잭션과 수평 확장을 제공한다.

결과: SQL 인터페이스는 SQLite 그대로, 내부 스토리지는 분산 트랜잭셔널 KV. 단일 DB 크기 한계가 사라지고, 멀티 노드가 같은 DB를 동시에 읽고 쓴다. 트레이드오프는 운영 — FoundationDB 자체가 운영이 만만하지 않다.

mvSQLite는 프로덕션 채택이 활발하다고 보기는 어렵지만, "SQLite의 API를 유지하면서 스토리지만 분산화한다" 는 디자인 어휘는 강력하다.

rqlite — Raft 위의 SQLite

rqlite는 SQLite 인스턴스 여러 개 앞에 Raft 합의 알고리즘 을 두고 강한 일관성을 만든 시스템이다. 모든 쓰기는 Raft 리더로 가고, 로그가 복제된 뒤에 각 노드의 SQLite에 적용된다.

특징:

  • CP 시스템 (CAP에서 일관성 우선).
  • 클러스터 크기는 보통 3~7대.
  • 매니지드 서비스가 아닌 자가 운영 이 기본.
  • HTTP API로 노출.

rqlite는 큰 회사보다는 엣지 디바이스(IoT, 산업용 게이트웨이)·소규모 클러스터·임베디드 분산 시나리오 에서 빛난다. "복잡한 분산 DB를 운영하기엔 작지만, SQLite의 단일 노드는 위험한" 케이스의 정답이다.

비교가 중요하다 — 다 같지 않다

시스템1차 디자인 축쓰기 토폴로지일관성
libSQL/Turso임베디드 레플리카 + 매니지드단일 1차, 비동기 복제결과적 (read-your-writes 옵션)
LiteFSFUSE 투명 복제 (일몰)단일 1차, 비동기 복제결과적
Cloudflare D1엣지 매니지드 + 읽기 레플리카단일 1차, 자동 리드 레플리카결과적 (읽기), 직렬화 (쓰기)
mvSQLiteFDB 백킹 분산 SQL멀티 라이터, FDB 합의직렬화 가능 (FDB 보장)
rqliteRaft 위의 SQLite리더 쓰기선형화 (Raft 보장)
평범한 SQLite + Litestream단일 노드 + S3 백업단일 라이터(백업/복구만)

이 표가 핵심이다 — "SQLite 기반"이라는 한 단어 안에 매우 다른 디자인이 들어 있다. 무엇을 풀고 싶은지 가 선택을 결정한다.


6장 · Litestream — 모두의 기반이 된 지속 백업

분산화를 원하지 않는다면? 그래도 백업은 필요하다. Litestream이 그 자리를 차지했다.

무엇을 하는가

Litestream은 SQLite의 WAL(Write-Ahead Log)을 거의 실시간으로 S3(또는 호환 스토리지)에 스트리밍 한다. 사이드카 프로세스로 돈다. 앱은 자기가 백업되고 있는지 모른다.

# 한 줄 셋업
litestream replicate /var/lib/app/data.db s3://my-bucket/backups/data.db

# 복구
litestream restore -o /var/lib/app/data.db s3://my-bucket/backups/data.db

복구는 PITR(Point-in-Time Recovery)이다 — 임의의 과거 시점으로 되돌릴 수 있다. RPO(복구 시점 목표)는 보통 초 단위.

왜 중요한가

Litestream은 "단일 노드 SQLite는 위험하다"는 마지막 정당화 를 깼다. 디스크가 죽어도, 인스턴스가 사라져도, 몇 분 안에 S3에서 복원된다. 운영 모델은 충격적일 만큼 단순하다 — 앱 1개, SQLite 파일 1개, Litestream 사이드카 1개. 끝.

LiteFS 일몰 이후 Fly와 다른 PaaS들이 Litestream을 더 추천하는 흐름이 강해졌다. "복잡한 분산 SQLite보다, 단일 노드 + Litestream이 90% 케이스에 더 낫다"는 입장이다.

저자(Ben Johnson, Fly.io)는 이후 LiteFS도 만든 사람인데, 본인이 직접 "대부분의 앱에는 Litestream이면 충분하다"고 자주 말한다. 통찰이다.


7장 · Bun SQLite + 네이티브 런타임 SQLite — 또 다른 부활의 축

런타임 레벨에서도 변화가 있다.

Bun의 bun:sqlite

Bun은 처음부터 bun:sqlite내장 모듈로 제공한다. 동기 API, 매우 빠르고, 의존성 0개.

import { Database } from 'bun:sqlite'

const db = new Database('app.db', { create: true })

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    email TEXT UNIQUE,
    created_at INTEGER DEFAULT (strftime('%s', 'now'))
  )
`)

// Prepared statement — N번 호출에도 한 번만 컴파일
const insert = db.prepare('INSERT INTO users (email) VALUES (?)')
const tx = db.transaction((emails: string[]) => {
  for (const e of emails) insert.run(e)
})
tx(['a@x.com', 'b@x.com', 'c@x.com'])

// Query
const all = db.query('SELECT id, email FROM users').all()
console.log(all)

이게 왜 새로운가? Node.js 진영에서는 SQLite를 쓰려면 외부 패키지(better-sqlite3, node-sqlite3)가 필요했다. Node.js 22+ 이후로는 실험적 node:sqlite가 들어왔다. 런타임이 SQLite를 1급 시민으로 받아들이기 시작한 것이다.

의미

이 흐름이 의미하는 것 — "앱 안에 SQL DB가 있다"는 모델이 더 이상 이상하지 않다. 별도 의존성 없이, 한 줄 임포트로. 이는 위에서 본 임베디드 레플리카 모델과 자연스럽게 결합한다. 런타임 자체가 SQLite를 알고, 라이브러리는 그 위에서 동기화를 한다.

Deno도, 그리고 모든 곳에서

Deno도 SQLite 표준 모듈을 제공한다. WASM 빌드의 SQLite(sql.js, wa-sqlite)는 브라우저에서 동작한다. 모든 런타임이 SQLite를 안다. 이 보편성이 SQLite를 "교환 표준" 으로 만든다 — 같은 파일을 서버, 클라이언트, 엣지가 모두 읽고 쓸 수 있다.


8장 · 로컬 우선 앱 — SQLite가 가능하게 한 패러다임

여기까지가 "서버 쪽 SQLite의 부활" 이라면, 더 큰 파도는 클라이언트다. 로컬 우선(local-first) 앱들.

정의

Ink & Switch의 정의를 요약하면 로컬 우선 앱은 다음을 만족한다:

  1. 즉각적 (네트워크 라운드트립 없음).
  2. 오프라인에서 동작.
  3. 다중 기기 동기화.
  4. 협업 가능.
  5. 데이터 소유권이 사용자에게.
  6. 보안과 프라이버시가 기본.
  7. 장기 수명 (서비스가 죽어도 데이터가 산다).

이를 위해서는 데이터가 클라이언트 안에 영속적으로 있어야 한다. 그 안의 SQL 엔진? SQLite다.

두 축은 다르다 — "로컬 우선" 과 "CRDT"

흔한 오해 하나를 짚자. "로컬 우선 = CRDT" 가 아니다. 둘은 직교한다.

  • 로컬 우선 은 데이터 토폴로지에 관한 약속이다 — 데이터가 클라이언트에 있다, 네트워크 없이도 동작한다.
  • CRDT(Conflict-free Replicated Data Type) 는 여러 클라이언트가 같은 데이터를 동시에 수정할 때 충돌을 해결하는 알고리즘이다.

협업이 없거나 단일 사용자가 다중 기기를 쓰는 경우(예: 개인 노트), 단순한 last-write-wins나 벡터 클럭으로 충분하다. CRDT는 다중 사용자 동시 편집에서 필요해진다(Figma, Linear 등).

SQLite는 로컬 우선의 영속성 기반 이다. CRDT 엔진(Automerge, Yjs)이나 SQL-네이티브 동기 엔진(ElectricSQL, Replicache, PowerSync)이 그 위에서 동기화를 다룬다.

SQL-네이티브 동기 엔진들

  • ElectricSQL — Postgres와 SQLite 사이의 양방향 동기. SQLite 안에 변경분이 모이고, 백그라운드로 Postgres와 머지. 충돌은 Rich-CRDT(reg. tree of operations)로 해결.
  • Replicache (Rocicorp) — 자체 데이터 모델, 클라이언트는 IndexedDB(또는 SQLite-WASM)를 쓰고, 서버는 push/pull 엔드포인트만 구현하면 됨.
  • PowerSync — Postgres 기반, 클라이언트는 SQLite. ElectricSQL과 경쟁/유사.

이들의 공통점: 클라이언트에 SQLite, 서버에 Postgres, 그 사이의 동기 레이어. SQLite의 부활은 단순히 서버 사이드만이 아니라, 이런 클라이언트-서버 토폴로지의 한쪽 끝을 책임지는 위치에서도 일어나고 있다.


9장 · SQLite가 옳을 때, 그리고 틀릴 때

여기까지 보면 SQLite 만능론처럼 들릴 수 있다. 그래서는 안 된다. 정직하게 보자.

SQLite가 옳은 케이스

  1. 읽기 헤비, 트래픽이 한 노드에 들어가는 앱. 대부분의 웹사이트, 대부분의 SaaS 백오피스, 거의 모든 블로그·문서 사이트. "QPS가 분당 1만 미만이고 단일 인스턴스로 충분" 이라면 SQLite가 종종 더 빠르다(네트워크 라운드트립이 없으므로).
  2. 엣지 네이티브 앱. Cloudflare Workers + D1, Fly.io + Litestream 모델. 데이터를 코드 옆에 두는 게 자연스럽다.
  3. 임베디드/IoT. 산업용 게이트웨이, 디바이스 로컬 로깅. SQLite의 본업.
  4. 로컬 우선 클라이언트. 데스크톱·모바일·PWA 앱의 영속 저장소.
  5. per-tenant 격리. 테넌트당 DB 1개. SQLite는 너무 가벼워서 수십만 개도 가능. Notion·Linear·Turso의 일부 디자인 패턴.
  6. 분석 쿼리의 임시 작업 공간. DuckDB가 더 어울리지만, 단순 케이스는 SQLite로도 충분.
  7. 테스트 환경의 격리된 DB. Postgres 대신 인메모리 SQLite. 빠르고 깨끗하다.

SQLite가 틀린 케이스

  1. 글로벌 단일 1차 + 다중 지역 동시 쓰기. 한 DB에 여러 지역에서 동시에 마이크로초 단위로 쓰기가 필요하면, SQLite 기반 시스템은 거의 다 단일 1차다. 다중 지역 쓰기는 Spanner, CockroachDB, YugabyteDB, FoundationDB 같은 진짜 분산 시스템의 영역이다.
  2. 쓰기 처리량이 코어 1개를 초과. SQLite는 쓰기에 단일 라이터 락이 있다(WAL 모드여도 트랜잭션 단위로 직렬화). 초당 수만 쓰기를 지속해야 한다면 다른 엔진을 봐라.
  3. 트랜잭션이 매우 길게 잡혀야 하는 OLTP. 긴 트랜잭션은 다른 쓰기를 막는다. Postgres MVCC가 훨씬 우아하다.
  4. 거대한 단일 DB. D1은 한 자릿수 GB가 한계, libSQL/Turso도 수십~수백 GB는 가능하지만 TB급 단일 DB는 어색하다. mvSQLite가 답일 수도 있지만, 그 시점에는 분산 OLTP를 다시 검토하라.
  5. 복잡한 분석 쿼리 (OLAP). SQLite 옵티마이저는 OLTP 워크로드에 맞춰져 있다. 대량 분석은 DuckDB·ClickHouse·Snowflake가 자기 자리다.
  6. 다중 라이터 어플리케이션이 데이터 무결성을 강하게 요구. 동시 쓰기 격리 수준이 필요하면 Postgres가 정답이다.
  7. 풀텍스트 검색 헤비. SQLite의 FTS5는 훌륭하지만 Elasticsearch·Meilisearch·Typesense 수준은 아니다.

결정 트리

시작:
  단일 노드에 들어가는가? (예: < 1만 QPS, < 100GB)
    └─ 예  → 다중 지역에 분산이 필요한가?
              ├─ 아니오 → SQLite + Litestream (가장 단순함)
              └─ 예    → 읽기/쓰기 비율은?
                         ├─ 읽기 헤비 → libSQL/Turso (임베디드 레플리카)
                         └─ 균형     → D1 (Workers라면) / Postgres + 리드 레플리카

    └─ 아니오 → 워크로드는?
                ├─ OLTP 분산   → CockroachDB / Spanner / YugabyteDB
                ├─ OLAP/분석   → ClickHouse / Snowflake / BigQuery
                └─ 멀티테넌트  → 테넌트당 SQLite (per-tenant) 진지하게 고려

이 결정 트리에서 가장 자주 답이 되는 것은 첫 번째 분기 다 — 대부분의 앱은 단일 노드에 들어간다. 그 사실을 인정하고 나면 운영의 단순함이 폭발적으로 좋아진다.


10장 · 운영의 실제 — 무엇을 진짜 신경 써야 하나

SQLite 기반 시스템을 운영할 때 자주 마주치는 함정들.

WAL 모드는 기본값으로

PRAGMA journal_mode=WAL;. 동시 읽기-쓰기 처리량이 극적으로 좋아진다. 거의 모든 라이브러리가 기본값으로 켜지만, 직접 만든 셋업이면 확인하라.

PRAGMA synchronous 의 트레이드오프

  • FULL (기본): 가장 안전, 가장 느림.
  • NORMAL: WAL 모드에서 권장. 매우 안전하고 훨씬 빠름.
  • OFF: 위험. 권장하지 않음.

백업은 무조건 Litestream 또는 동급

단일 노드 SQLite의 최대 위험은 디스크/인스턴스 손실 이다. Litestream으로 분 단위 RPO를 확보하라. EBS 스냅샷도 보완책으로 좋다.

분리 마이그레이션 + 앱 배포

SQLite 마이그레이션은 보통 빠르지만, 큰 테이블의 ALTER TABLE은 락이 걸린다. 큰 변경은 새 테이블 + 백필 + 스왑 패턴을 써라.

임베디드 레플리카의 동기 주기

Turso 임베디드 레플리카에서 syncInterval을 너무 짧게 잡으면(예: 1초) 네트워크와 CPU를 낭비한다. 보통 30~60초가 좋은 출발점이고, 쓰기 직후 즉시성이 필요한 곳에서는 명시적 db.sync()나 read-your-writes 모드를 써라.

per-tenant DB의 메타데이터 관리

테넌트당 DB 1개 패턴을 쓰면, "어느 테넌트 DB가 어느 인스턴스/리전에 있는지" 라우팅 메타데이터가 필요하다. 이 메타데이터 자체는 한 개의 중앙 DB(보통 Postgres 또는 더 큰 SQLite)에 둔다.


에필로그 — 부활은 우연이 아니다

24년 된 라이브러리가 가장 핫한 인프라가 된 것은 우연이 아니다. 세 가지가 동시에 일어났다 — 엣지 컴퓨트의 보편화, 로컬 우선 운동의 성숙, 그리고 "분산이 답이 아닐 수도 있다" 는 자각.

이 글의 한 줄 요약: SQLite는 새 도구가 아니라, 새 토폴로지를 가능하게 한 오래된 도구다. libSQL/Turso의 임베디드 레플리카, Cloudflare D1의 엣지 통합, Litestream의 백업 기반, Bun과 Node의 네이티브 SQLite — 이 모든 것이 같은 방향을 가리킨다. 데이터를 코드 옆에 두라.

체크리스트 — SQLite 기반 시스템 시작하기 전

  • 단일 노드에 들어가는가? (QPS, 데이터 크기 추산)
  • 읽기/쓰기 비율을 측정했는가? (90/10 이상이면 매우 유리)
  • 백업 전략이 있는가? (Litestream 또는 동급)
  • WAL 모드, synchronous=NORMAL 켰는가?
  • 마이그레이션 패턴을 정했는가?
  • 멀티 지역이 필요하다면, libSQL/Turso 또는 D1을 골랐는가?
  • 운영 단순성의 이점이 다른 DB의 분산 기능 손실을 상쇄하는가?

안티 패턴

  • "SQLite는 장난감 DB" 라는 편견으로 시작도 안 함. Notion, Linear, Cloudflare 등이 프로덕션에서 쓴다.
  • 거꾸로, SQLite로 다 풀려는 만능론. 9장의 "틀린 케이스" 를 인정하라.
  • 백업 없는 단일 노드 SQLite. 디스크는 죽는다.
  • 임베디드 레플리카에 강한 일관성을 기대. 모델 자체가 결과적 일관성이다.
  • LiteFS를 새 프로젝트에 도입. 일몰 흐름이다. libSQL이나 D1을 봐라.
  • 너무 잦은 db.sync() 호출. 60초 + 명시적 호출 조합이 보통 좋다.
  • 모든 분산 SQLite 시스템이 같은 보장을 한다고 가정. 표를 다시 보라 — rqlite는 선형화, Turso는 결과적, D1은 쓰기 직렬화 + 결과적 읽기다.

다음 글 예고

다음 두 편에서는 SQLite 르네상스의 자매 주제 두 가지를 다룬다:

  1. 로컬 우선 앱 구축 실습 — ElectricSQL과 PowerSync로 Postgres ↔ SQLite 양방향 동기 구현하기. 충돌 해결, 오프라인 큐, 모바일 클라이언트.
  2. 2026년의 데이터베이스 지도 — SQLite 르네상스, Postgres 18의 새 기능, DuckDB의 분석 침공, NewSQL의 현실, 그리고 벡터 DB가 결국 Postgres로 흡수되는 흐름.

24살 된 단일 파일 라이브러리가 가장 흥미로운 곳에 있다는 사실은 우리 업계의 진리 하나를 다시 가르친다 — 단순함은 다시 트렌드가 된다. 충분히 오래 기다리면.


참고 / References

The SQLite Renaissance of 2026 — How a 24-Year-Old Single-File Database Became the Hottest Infrastructure (libSQL, Turso, LiteFS, D1, mvSQLite Deep Dive)

Prologue — Why Did a 24-Year-Old Database Get Hot Again

Look at a 2026 infrastructure stack and you see an odd tableau. On one side, Postgres 18 is polishing multi-master logical replication. On another, distributed NewSQL promises global consistency. But the loudest trend is neither of those — SQLite, the embedded library D. Richard Hipp shipped in 2000 as a single file, is back at the center of the stage.

SQLite is not a database. It is a library. That is the secret of this revival.

It sounds strange but reconsider. The two macro pressures on 2026 infrastructure are (a) compute moving to the edge and (b) local-first UX. Both have the same answer — "eliminate the network round-trip". And to eliminate the network, data has to be next to the code. What kind of database sits next to your code? A local file. SQLite has always been a local file.

This post dissects how a 24-year-old single-file database became 2026's most interesting infrastructure category. Exactly what is new — the libSQL fork and Turso's embedded replicas, Cloudflare D1, the now-sunset LiteFS, FoundationDB-backed mvSQLite, Litestream backup — and what topology made SQLite into an "edge database". And honestly, when SQLite is right and when it is absolutely wrong.


1. The SQLite-at-the-Edge Thesis — Why Now

Let us be clear. SQLite is not new. 1.0 shipped in 2000. It is in every Android, iOS, macOS, every browser, almost every IoT device. It is the most-deployed database on Earth. So why has it suddenly become a 2026 "trend"?

Three things happened simultaneously.

First, edge compute went mainstream. Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Fly.io — code moved next to users. But the database? Still in us-east-1. A user in Seoul calls a function running in a Tokyo edge that then makes a 200ms round-trip to a Postgres in us-east-1. The whole point of the edge is gone.

Second, the local-first movement matured. Ink and Switch's 2019 essay seeded a wave that, by 2026, has crystallized into real products — parts of Linear, Figma, Notion. The promise: "works offline, instant, your data on your device." That requires data inside the client. The embedded SQL DB that fits in a client? SQLite.

Third, the myth that "scale-out is the answer" cracked. In the early 2020s every company chanted "distributed distributed distributed", but by 2026 nearly everyone has realized — most apps do not need distributed consistency. They fit on one node. And a single-node database makes operations almost embarrassingly simple.

The intersection of those three is SQLite. Local file, embedded, small, trustworthy. And the new tooling of the mid-2020s started patching SQLite's traditional weaknesses — distribution, replication, backup. That is the essence of the renaissance.


2. libSQL and Turso — The "Embedded Replica" Invention

If you have to pick the single most influential piece of this revival, it is Turso's embedded replica model.

libSQL — The Open Fork of SQLite

SQLite itself is open source but does not accept external contributions. The SQLite team's position is "the code is in the public domain, but we write it." That created an itch — people wanted to add features (pluggable storage backends, native replication, better concurrency) to SQLite but had no path.

So around 2022 to 2023 Turso (then ChiselStrike) created libSQL, a real fork. MIT-licensed, open to external contributions, and most importantly — adding a native network protocol (HTTP and WebSocket via "Hrana"), user-defined functions, and embedded replicas.

Embedded Replicas — The Model Itself Is New

Traditional model: app to network to DB. Every query round-trips.

Turso's embedded replica: there is a SQLite file inside your app. Reads happen on that local file — microseconds. Writes go to a remote primary, and the primary pushes change deltas (WAL frames) back to sync your local file in the background.

+------------------------+                    +------------------+
|       App (e.g. edge)  |                    |  Turso primary   |
|  +------------------+  |  Reads: local disk |  (global primary)|
|  | libSQL client    +--+   sub-millisecond  |                  |
|  |  + local .db     |                       |                  |
|  +--------+---------+                       |                  |
|           | Writes (HTTP / Hrana) --------->|                  |
|           | <-- WAL frames (background sync)|                  |
+------------------------+                    +------------------+

What is so new here? It is the first time the asymmetry of reads and writes is honestly embraced. Most web apps are 99 percent reads. If you can make reads sub-millisecond, you can afford the round-trip on writes. The classic "read replica" was always across the network. Turso's embedded replica brings that replica into the app process as a file on disk.

Code — Turso Embedded Replica Setup

// pnpm add @libsql/client
import { createClient } from '@libsql/client'

const db = createClient({
  // Local SQLite file (it really exists on disk)
  url: 'file:local.db',
  // Remote primary — writes go here
  syncUrl: 'libsql://my-db-myteam.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN,
  // Background sync cadence
  syncInterval: 60, // seconds
})

// First sync — pull remote state into the local file
await db.sync()

// Read — straight from the local file. No network.
const rows = await db.execute('SELECT * FROM posts WHERE published = 1')

// Write — goes remote, then the local file catches up on next sync
await db.execute({
  sql: 'INSERT INTO posts (title, body) VALUES (?, ?)',
  args: ['Hello', 'World'],
})

// You can also force-sync
await db.sync()

The implications are large, the largest being — reads are as fast as local file IO. A SQLite index lookup on SSD is in microseconds. No distributed DB's cache layer can beat that. The trade-off is consistency. The local replica only sees data as of the last sync. If you must see your own write immediately, you turn on read-your-writes mode (that one query round-trips to the primary), and workloads that need strong consistency should just use remote mode.

The Turso Platform Itself

Turso layered a managed service on top. A generous free tier (hundreds of millions of rows free), database branching (branch your DB like a Git branch), and multi-region primaries are available. Since 2024 Turso's biggest bet has been per-tenant databases — one DB per user. SQLite is light enough that hundreds of thousands of tiny DBs are feasible. A new model for multi-tenancy.


3. LiteFS — Fly's Distributed SQLite Experiment and Its Sunset

Fly.io tried a different answer in 2022 — LiteFS, a FUSE filesystem layer.

The LiteFS Idea

Mount a plain SQLite file on a FUSE filesystem, and LiteFS intercepts every transaction (WAL frame) and replicates it to other nodes. The app does not know it is in a distributed environment — it just opens a SQLite file. One node is the "primary" and all writes are routed there (the LiteFS Proxy handles this automatically). Other nodes receive replicas in near-real-time.

The biggest attraction was no app-code changes. Rails, Django, Phoenix — any framework that can open a SQLite file ran in distributed mode on LiteFS.

The Sunset

Then in mid-2024, Fly announced that LiteFS development was effectively paused, and through 2025 the signals got stronger that it was no longer recommended for new users (the phrase "maintenance mode" comes up frequently in official posts). Technically the cited reasons were the complexity of FUSE itself, the difficulty of multi-primary, and operational burden. Fly's managed SQLite vision moved in other directions (a stronger Fly Postgres, partnerships such as Tigris, and so on).

What to Take Away

LiteFS's sunset is not a failure of the idea. LiteFS was one of the cleanest answers to "how do you distribute SQLite" — app-transparent, single-primary, WAL-level replication. Even though it did not survive as a managed offering, libSQL and D1 inherit pieces of that design vocabulary — primary plus async replication, WAL-frame-level sync.

If you are starting a distributed SQLite project today, I would not recommend LiteFS. But those design docs are still worth reading.


4. Cloudflare D1 — The Managed Answer for Edge SQLite

Cloudflare's answer is different. D1 brings SQLite itself next to the Workers runtime.

The D1 Model

  • Storage unit: a SQLite database.
  • Hosting: on Cloudflare's global network, modeled as a primary region plus automatic read replicas.
  • Access: via Workers bindings (env.DB.prepare(...)). There is also an HTTP API, but the primary model is in-Workers calls.
  • Status: as of 2026 it is GA, with "Global Read Replication" placing read replicas on multiple continents automatically. Per-database size is in the single-digit GBs (around 10GB today), so it fits per-application or per-tenant models better than giant monolithic DBs.

Code Example — Workers + D1

// Assumes wrangler.toml has a D1 binding
// [[d1_databases]]
// binding = "DB"
// database_name = "blog"
// database_id = "..."

export interface Env {
  DB: D1Database
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url)
    const slug = url.pathname.slice(1)

    // prepare + bind — safe from SQL injection
    const stmt = env.DB.prepare(
      'SELECT title, body, published_at FROM posts WHERE slug = ?1 LIMIT 1'
    ).bind(slug)

    const row = await stmt.first<{ title: string; body: string; published_at: string }>()

    if (!row) return new Response('Not found', { status: 404 })

    return Response.json(row)
  },
}

The key is that env.DB is not a network handle but a binding the runtime injects. Workers and D1 run on the same infrastructure, and Cloudflare hides routing, pooling, and replication. From the developer's point of view, there is just "SQL inside the edge function."

Trade-offs

  • Write consistency: serialized on a single primary. No multi-region writes. No promise of global consistency.
  • Transactions: interactive transactions are limited. The batch API is preferred — bundle multiple statements into one call.
  • Size: single-digit GBs per DB. Larger data partitions across R2 plus D1 metadata.
  • Lock-in: D1 shines inside the Workers ecosystem. Elsewhere the appeal shrinks.

D1's real value is "if you use Workers, you get persistence with almost no friction". It is the most integrated implementation of SQLite-at-the-edge.


5. Other Paths for Distributed SQLite — mvSQLite and rqlite

The three above (libSQL/Turso, LiteFS, D1) are the mainstream, but there are more interesting experiments.

mvSQLite — Backed by FoundationDB

mvSQLite swapped SQLite's entire storage engine (VFS layer). Data is not on local disk but in FoundationDB. FoundationDB is the distributed transactional key-value store Apple uses, offering serializable transactions and horizontal scale.

Result: the SQL interface is plain SQLite, but the underlying storage is distributed transactional KV. The single-DB size cap is gone, and multiple nodes can simultaneously read and write the same DB. The trade-off is operations — FoundationDB itself is not trivial to operate.

mvSQLite does not see heavy production adoption, but its design vocabulary — keep SQLite's API and only distribute the storage — is powerful.

rqlite — SQLite on Top of Raft

rqlite puts the Raft consensus algorithm in front of several SQLite instances to get strong consistency. All writes go to the Raft leader, and once the log is replicated, the entries are applied to each node's SQLite.

Characteristics:

  • A CP system (CAP prioritizing consistency).
  • Cluster size is typically three to seven nodes.
  • Self-hosted rather than managed.
  • Exposed as HTTP.

rqlite shines for edge devices (IoT, industrial gateways), small clusters, and embedded distributed scenarios rather than for giant companies. It is the right answer for "too small to justify a real distributed DB but single-node SQLite is too risky."

The Comparison Matters — Not All the Same

SystemPrimary design axisWrite topologyConsistency
libSQL / TursoEmbedded replicas plus managedSingle primary, async replicationEventual (read-your-writes optional)
LiteFSFUSE-transparent replication (sunset)Single primary, async replicationEventual
Cloudflare D1Edge managed plus read replicasSingle primary, automatic read replicasEventual (reads), serialized (writes)
mvSQLiteFDB-backed distributed SQLMulti-writer, FDB consensusSerializable (FDB guarantee)
rqliteSQLite on RaftLeader writesLinearizable (Raft guarantee)
Plain SQLite plus LitestreamSingle node plus S3 backupSingle writer(Backup and restore only)

This table is the heart of the chapter — "SQLite-based" hides very different designs underneath. What you want to solve decides what you pick.


6. Litestream — The Durable Backup Beneath Everything

What if you do not want distribution? You still need backups. Litestream took that role.

What It Does

Litestream streams the SQLite WAL (Write-Ahead Log) to S3 (or compatible storage) in near-real-time. It runs as a sidecar process. The app does not know it is being backed up.

# One-line setup
litestream replicate /var/lib/app/data.db s3://my-bucket/backups/data.db

# Restore
litestream restore -o /var/lib/app/data.db s3://my-bucket/backups/data.db

Restore is PITR (Point-In-Time Recovery) — you can roll back to any past instant. RPO (Recovery Point Objective) is in seconds.

Why It Matters

Litestream broke the last justification for the "single-node SQLite is risky" objection. Disk dies, instance vanishes, and within minutes you restore from S3. The operating model is shockingly simple — one app, one SQLite file, one Litestream sidecar. Done.

After LiteFS's sunset, Fly and other PaaS providers have leaned more on Litestream. "Instead of complex distributed SQLite, single-node plus Litestream is better for 90 percent of cases" is the position.

The author (Ben Johnson at Fly.io) also created LiteFS, and he himself often says "Litestream is enough for most apps." That is an honest insight.


7. Bun SQLite and Native Runtime SQLite — Another Axis of the Revival

There are also changes at the runtime level.

Bun's bun:sqlite

Bun ships bun:sqlite as a built-in module. Synchronous API, very fast, zero dependencies.

import { Database } from 'bun:sqlite'

const db = new Database('app.db', { create: true })

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    email TEXT UNIQUE,
    created_at INTEGER DEFAULT (strftime('%s', 'now'))
  )
`)

// Prepared statement — compiled once even if called N times
const insert = db.prepare('INSERT INTO users (email) VALUES (?)')
const tx = db.transaction((emails: string[]) => {
  for (const e of emails) insert.run(e)
})
tx(['a@x.com', 'b@x.com', 'c@x.com'])

// Query
const all = db.query('SELECT id, email FROM users').all()
console.log(all)

Why is this new? In the Node.js world, using SQLite required an external package (better-sqlite3, node-sqlite3). Since Node.js 22+, the experimental node:sqlite ships in core. Runtimes are starting to treat SQLite as a first-class citizen.

What It Means

The implication of this trend — "there is a SQL DB inside my app" is no longer weird. No external dependencies, one-line import. It pairs naturally with the embedded-replica model above. The runtime knows SQLite, and the library does the sync on top.

Deno Too, and Everywhere Else

Deno also ships a standard SQLite module. WASM builds of SQLite (sql.js, wa-sqlite) run in browsers. Every runtime knows SQLite. That ubiquity makes SQLite an "exchange standard" — the same file can be read and written by the server, the client, and the edge.


8. Local-First Apps — The Paradigm SQLite Enables

If everything above is "the revival on the server side", the bigger wave is on the client. Local-first apps.

The Definition

To summarize Ink and Switch's definition, a local-first app satisfies these properties:

  1. Instant (no network round-trip).
  2. Works offline.
  3. Syncs across devices.
  4. Supports collaboration.
  5. Data ownership stays with the user.
  6. Security and privacy by default.
  7. Long-lived (data outlives the service).

That requires data persistent inside the client. The SQL engine inside? SQLite.

The Two Axes Are Different — "Local-First" and "CRDT"

A common misconception to flag — "local-first" is not the same as "CRDT". They are orthogonal.

  • Local-first is a promise about data topology — data lives on the client, the app works without a network.
  • CRDT (Conflict-free Replicated Data Type) is an algorithm to resolve conflicts when multiple clients edit the same data simultaneously.

For no-collaboration cases or single-user multi-device scenarios (a personal note app), simple last-write-wins or vector clocks suffice. CRDTs become necessary for multi-user concurrent editing (Figma, Linear, etc.).

SQLite is the persistence foundation for local-first. The CRDT engines (Automerge, Yjs) and SQL-native sync engines (ElectricSQL, Replicache, PowerSync) handle the sync on top.

SQL-Native Sync Engines

  • ElectricSQL — bidirectional sync between Postgres and SQLite. Changes accumulate inside SQLite and merge with Postgres in the background. Conflicts resolve via Rich-CRDT (a registered tree of operations).
  • Replicache (Rocicorp) — its own data model. The client uses IndexedDB (or SQLite WASM), and the server only has to implement push and pull endpoints.
  • PowerSync — Postgres-based, SQLite on the client. Competes with and resembles ElectricSQL.

What they have in common — SQLite on the client, Postgres on the server, sync layer in between. The SQLite revival is not just server-side; SQLite holds one end of this client-server topology as well.


9. When SQLite Is Right, and When It Is Wrong

By now this could read as SQLite triumphalism. It should not. Be honest.

When SQLite Is Right

  1. Read-heavy app whose traffic fits on one node. Most websites, most SaaS back-offices, almost all blog and docs sites. If "under 10k QPM and a single instance is enough", SQLite is often faster (no network round-trip).
  2. Edge-native apps. The Cloudflare Workers plus D1 or the Fly.io plus Litestream model. Data next to code is natural.
  3. Embedded and IoT. Industrial gateways, device-local logging. SQLite's day job.
  4. Local-first clients. Persistent storage for desktop, mobile, PWA apps.
  5. Per-tenant isolation. One DB per tenant. SQLite is light enough that hundreds of thousands are feasible. Design patterns from parts of Notion, Linear, and Turso.
  6. Scratch space for analytical queries. DuckDB fits better, but simple cases work with SQLite too.
  7. Isolated DBs for tests. In-memory SQLite instead of Postgres. Fast and clean.

When SQLite Is Wrong

  1. Global single primary plus simultaneous multi-region writes. If you need many regions writing to a single DB concurrently at sub-millisecond level, almost every SQLite-based system has a single primary. Multi-region writes are the domain of Spanner, CockroachDB, YugabyteDB, FoundationDB — real distributed systems.
  2. Write throughput beyond one core. SQLite has a single-writer lock on writes (even in WAL mode, transactions serialize). If you need tens of thousands of sustained writes per second, look elsewhere.
  3. OLTP that holds very long transactions. Long transactions block other writes. Postgres MVCC is much more graceful.
  4. A huge single DB. D1 caps in the single-digit GBs, libSQL and Turso scale to tens to hundreds of GB but TB-scale single DBs feel awkward. mvSQLite could be the answer, but at that point reconsider distributed OLTP.
  5. Complex analytical queries (OLAP). The SQLite optimizer is tuned for OLTP. Heavy analytics belong to DuckDB, ClickHouse, Snowflake.
  6. Multi-writer apps that strongly require data integrity. If you need real concurrent-write isolation levels, Postgres is the right answer.
  7. Full-text search heavy. SQLite's FTS5 is great but not Elasticsearch or Meilisearch or Typesense level.

Decision Tree

Start:
  Does it fit on a single node? (e.g. under 10k QPS, under 100GB)
    +-- Yes -> Need multi-region distribution?
              +-- No  -> SQLite plus Litestream (simplest)
              +-- Yes -> What is the read/write ratio?
                         +-- Read-heavy -> libSQL/Turso (embedded replicas)
                         +-- Balanced   -> D1 (on Workers) / Postgres plus read replicas

    +-- No  -> What is the workload?
                +-- Distributed OLTP -> CockroachDB / Spanner / YugabyteDB
                +-- OLAP / analytics -> ClickHouse / Snowflake / BigQuery
                +-- Multi-tenant     -> Seriously consider per-tenant SQLite

The most common answer in this tree is the first branch — most apps fit on one node. Accepting that fact unlocks a huge improvement in operational simplicity.


10. The Reality of Operations — What to Really Care About

Common pitfalls in operating SQLite-based systems.

WAL Mode by Default

PRAGMA journal_mode=WAL;. Concurrent read-write throughput improves dramatically. Almost every library defaults it on, but if you built the setup yourself, double check.

Trade-offs of PRAGMA synchronous

  • FULL (default): safest, slowest.
  • NORMAL: recommended in WAL mode. Very safe and much faster.
  • OFF: dangerous. Not recommended.

Backups Are Litestream or Equivalent, No Exception

The biggest risk of a single-node SQLite is disk or instance loss. Use Litestream to lock in a minute-scale RPO. EBS snapshots are a fine complement.

Migration and App Deploy Decoupled

SQLite migrations are usually fast, but ALTER TABLE on a large table takes a lock. For big changes use the new-table-plus-backfill-plus-swap pattern.

Sync Cadence for Embedded Replicas

For Turso embedded replicas, setting syncInterval too short (one second) wastes network and CPU. 30 to 60 seconds is a good starting point, and where immediate visibility after a write matters, use an explicit db.sync() or read-your-writes mode.

Metadata Management for Per-Tenant DBs

If you adopt one-DB-per-tenant, you need routing metadata for "which tenant DB is on which instance and region". That metadata itself lives in one central DB (typically Postgres or a larger SQLite).


Epilogue — The Revival Is Not an Accident

A 24-year-old library becoming the hottest infrastructure is not an accident. Three things happened simultaneously — edge compute became universal, the local-first movement matured, and the realization that "distributed may not be the answer" sank in.

One-line summary of this post: SQLite is not a new tool but an old one that enables a new topology. Turso's embedded replicas, Cloudflare D1's edge integration, Litestream's backup foundation, native SQLite in Bun and Node — they all point the same way. Put your data next to your code.

Checklist — Before Starting on a SQLite-Based System

  • Does it fit on a single node? (Estimate QPS, data size.)
  • Did you measure the read/write ratio? (90/10 or higher is very favorable.)
  • Is there a backup strategy? (Litestream or equivalent.)
  • Did you enable WAL mode and synchronous=NORMAL?
  • Did you decide on a migration pattern?
  • If you need multi-region, did you choose libSQL/Turso or D1?
  • Does the operational simplicity offset losing other DBs' distribution features?

Anti-Patterns

  • Refusing to start because "SQLite is a toy DB". Notion, Linear, Cloudflare and others run it in production.
  • Conversely, treating SQLite as a hammer for everything. Respect chapter 9's "wrong cases".
  • A single-node SQLite without backups. Disks die.
  • Expecting strong consistency from embedded replicas. The model itself is eventually consistent.
  • Adopting LiteFS in a new project. It is sunsetting. Look at libSQL or D1.
  • Calling db.sync() too often. 60 seconds plus explicit calls is usually a good mix.
  • Assuming all distributed SQLite systems give the same guarantees. Reread the table — rqlite is linearizable, Turso is eventual, D1 is serialized writes plus eventual reads.

Coming Up Next

The next two posts will cover sibling topics of the SQLite renaissance:

  1. Building local-first apps in practice — bidirectional Postgres-to-SQLite sync with ElectricSQL and PowerSync. Conflict resolution, offline queues, mobile clients.
  2. The 2026 map of databases — the SQLite renaissance, Postgres 18's new features, DuckDB's analytics invasion, the reality of NewSQL, and the trend of vector DBs being absorbed back into Postgres.

The fact that a 24-year-old single-file library sits in the most interesting place reaffirms one of our industry's lessons — simplicity becomes trendy again. If you wait long enough.


참고 / References