Skip to main content

better-sqlite3 vs libsql vs sql.js: SQLite in Node.js (2026)

·PkgPulse Team

TL;DR

better-sqlite3 is the fastest SQLite driver for Node.js — synchronous API (which is actually ideal for SQLite's single-writer model), C++ native extension, and used by default in Drizzle ORM's SQLite mode. libsql (from Turso) is a fork of SQLite with extensions — it powers the Turso serverless SQLite platform and supports both local file mode and remote HTTP connections. sql.js compiles SQLite to WebAssembly — runs in the browser, Cloudflare Workers, and any WASM-compatible environment, but requires loading a ~1MB WASM binary. In 2026: better-sqlite3 for server-side Node.js apps, libsql for Turso-connected or edge deployments, sql.js for browser-side SQLite.

Key Takeaways

  • better-sqlite3: ~3M weekly downloads — synchronous API, fastest Node.js SQLite driver, native C++
  • libsql: ~1M weekly downloads — Turso's SQLite fork, HTTP remote connections, edge-compatible
  • sql.js: ~500K weekly downloads — WebAssembly SQLite, browser + Workers, no native deps
  • SQLite is synchronous by design — better-sqlite3's sync API matches this perfectly
  • Bun has built-in SQLite (bun:sqlite) — no library needed, same API as better-sqlite3
  • libsql supports libsql:// URLs for Turso cloud + file: for local SQLite

SQLite Use Cases in 2026

When SQLite makes sense:
  - Development databases (no setup, just a .db file)
  - Edge deployments (low-overhead, embeddable)
  - Desktop apps with Electron/Tauri
  - Serverless functions with local storage (Cloudflare D1, Turso)
  - Analytics and read-heavy workloads
  - Single-region apps with moderate traffic

When to use PostgreSQL/MySQL instead:
  - Multiple concurrent writers at high volume
  - Multi-region horizontal scaling
  - Complex transactions across many tables
  - Team of developers hitting same DB concurrently

better-sqlite3

better-sqlite3 — the fast synchronous SQLite driver:

Basic usage

import Database from "better-sqlite3"

// Open (or create) a database:
const db = new Database("pkgpulse.db")

// Or: in-memory database:
const memdb = new Database(":memory:")

// Create tables:
db.exec(`
  CREATE TABLE IF NOT EXISTS packages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    version TEXT NOT NULL,
    weekly_downloads INTEGER DEFAULT 0,
    health_score REAL DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE INDEX IF NOT EXISTS idx_packages_name ON packages(name);
  CREATE INDEX IF NOT EXISTS idx_packages_health ON packages(health_score);
`)

// Insert:
const insert = db.prepare(`
  INSERT INTO packages (name, version, weekly_downloads, health_score)
  VALUES (@name, @version, @weeklyDownloads, @healthScore)
`)

insert.run({
  name: "react",
  version: "18.3.1",
  weeklyDownloads: 25_000_000,
  healthScore: 92.5,
})

// Prepared statements are fast — prepare once, run many times:
const insertMany = db.transaction((packages: Package[]) => {
  for (const pkg of packages) {
    insert.run(pkg)
  }
})

insertMany(packageList)  // All inserted in a single transaction

Queries

import Database from "better-sqlite3"

const db = new Database("pkgpulse.db")

// Get one row:
const getPackage = db.prepare("SELECT * FROM packages WHERE name = ?")
const react = getPackage.get("react")
// { id: 1, name: "react", version: "18.3.1", weekly_downloads: 25000000, health_score: 92.5 }

// Get all matching rows:
const getHealthy = db.prepare(`
  SELECT * FROM packages
  WHERE health_score >= @minScore
  ORDER BY weekly_downloads DESC
  LIMIT @limit
`)

const healthy = getHealthy.all({ minScore: 80, limit: 20 })
// Returns array of row objects

// Iterate without loading all into memory:
const stmt = db.prepare("SELECT * FROM packages ORDER BY weekly_downloads DESC")
for (const pkg of stmt.iterate()) {
  console.log(pkg.name, pkg.weekly_downloads)
}

// Count:
const { count } = db.prepare("SELECT COUNT(*) as count FROM packages").get() as { count: number }

Transactions

// better-sqlite3 transactions are synchronous and fast:
const updateStats = db.transaction((updates: Array<{ name: string; downloads: number }>) => {
  const stmt = db.prepare(`
    UPDATE packages SET weekly_downloads = @downloads WHERE name = @name
  `)

  let updated = 0
  for (const { name, downloads } of updates) {
    const result = stmt.run({ name, downloads })
    updated += result.changes
  }

  return updated  // Return value from transaction function
})

const count = updateStats(downloadUpdates)
console.log(`Updated ${count} packages`)

WAL mode and pragmas

import Database from "better-sqlite3"

const db = new Database("pkgpulse.db")

// Enable WAL mode — better concurrent read performance:
db.pragma("journal_mode = WAL")

// Performance pragmas:
db.pragma("synchronous = NORMAL")   // Faster writes (vs FULL), safe with WAL
db.pragma("cache_size = -64000")    // 64MB page cache
db.pragma("temp_store = MEMORY")    // Temp tables in memory
db.pragma("mmap_size = 268435456")  // 256MB memory-mapped I/O

// Check integrity:
const result = db.pragma("integrity_check")
console.log(result)  // [{integrity_check: "ok"}]

// Close:
process.on("exit", () => db.close())

libsql

libsql / @libsql/client — SQLite with superpowers:

Local SQLite mode (compatible with better-sqlite3)

import { createClient } from "@libsql/client"

// Local file — same as SQLite:
const db = createClient({ url: "file:pkgpulse.db" })

// In-memory:
const memdb = createClient({ url: ":memory:" })

// Async API (unlike better-sqlite3):
await db.execute(`
  CREATE TABLE IF NOT EXISTS packages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    health_score REAL DEFAULT 0
  )
`)

await db.execute({
  sql: "INSERT INTO packages (name, health_score) VALUES (?, ?)",
  args: ["react", 92.5],
})

const result = await db.execute("SELECT * FROM packages WHERE health_score > 80")
console.log(result.rows)
// [{ id: 1, name: "react", health_score: 92.5 }]

Turso remote connection

import { createClient } from "@libsql/client"

// Connect to Turso cloud database:
const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,      // libsql://dbname.turso.io
  authToken: process.env.TURSO_AUTH_TOKEN!,
})

// Embedded replicas (local SQLite + sync with Turso cloud):
const embeddedDb = createClient({
  url: "file:local-replica.db",         // Local SQLite file
  syncUrl: process.env.TURSO_DATABASE_URL!,  // Primary in Turso cloud
  authToken: process.env.TURSO_AUTH_TOKEN!,
})

// Sync local replica with cloud primary:
await embeddedDb.sync()

// Now reads are local (fast), writes go to cloud and sync back

With Drizzle ORM

import { drizzle } from "drizzle-orm/libsql"
import { createClient } from "@libsql/client"
import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core"
import { eq, gt } from "drizzle-orm"

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
})

const db = drizzle(client)

const packages = sqliteTable("packages", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
  healthScore: integer("health_score"),
})

// Type-safe queries:
const result = await db
  .select()
  .from(packages)
  .where(gt(packages.healthScore, 80))
  .limit(20)

sql.js

sql.js — SQLite compiled to WebAssembly:

Browser usage

import initSqlJs from "sql.js"

// Load WASM binary (must be served as a static asset):
const SQL = await initSqlJs({
  locateFile: (file) => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`,
})

// Create in-memory database:
const db = new SQL.Database()

// Execute SQL:
db.run(`
  CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT, score REAL);
  INSERT INTO packages VALUES (1, 'react', 92.5);
  INSERT INTO packages VALUES (2, 'vue', 89.0);
`)

// Query:
const stmt = db.prepare("SELECT * FROM packages WHERE score > $minScore")
stmt.bind({ $minScore: 85 })

while (stmt.step()) {
  const row = stmt.getAsObject()
  console.log(row)  // { id: 1, name: "react", score: 92.5 }
}

stmt.free()

// Export database as binary (save to IndexedDB or download):
const data = db.export()
const buffer = Buffer.from(data)
// Or: localStorage.setItem("db", btoa(String.fromCharCode(...data)))

Load existing SQLite file in browser

// Fetch a .db file and open it:
const response = await fetch("/data/packages.db")
const arrayBuffer = await response.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)

const SQL = await initSqlJs({ locateFile: (f) => `/wasm/${f}` })
const db = new SQL.Database(uint8Array)

const result = db.exec("SELECT name, score FROM packages ORDER BY score DESC LIMIT 10")
// [{columns: ["name", "score"], values: [["react", 92.5], ["vue", 89.0]]}]

Feature Comparison

Featurebetter-sqlite3libsqlsql.js
API styleSynchronousAsyncSync
Browser support
Cloudflare Workers✅ (D1/Turso)
Remote database✅ Turso
Native deps✅ C++✅ C++❌ WASM
Bun compatibility
ORM support✅ (Drizzle, Prisma)✅ (Drizzle)
WAL mode
TypeScript
Weekly downloads~3M~1M~500K

When to Use Each

Choose better-sqlite3 if:

  • Server-side Node.js app (not browser/edge)
  • Want maximum SQLite performance
  • Using Drizzle ORM or Prisma with SQLite
  • Building a desktop app (Electron, Tauri) with SQLite storage

Choose libsql if:

  • Using Turso for serverless SQLite hosting
  • Need embedded replica pattern (local reads + cloud writes)
  • Deploying to edge environments that support the libsql HTTP protocol
  • Want a migration path from local SQLite to cloud-hosted

Choose sql.js if:

  • Browser-side SQLite (offline web apps, client-side data processing)
  • Cloudflare Workers with SQLite in WASM
  • Need to open existing .db files in the browser
  • Deno or environments without native module support

Also consider:

  • Bun's built-in SQLite (import { Database } from "bun:sqlite") — zero-dep, better-sqlite3-compatible API, fastest option in Bun
  • Cloudflare D1 — uses SQLite API, global distribution, REST HTTP access

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on better-sqlite3 v9.x, @libsql/client v0.x, and sql.js v1.x.

Compare database driver packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.