Using Bun’s Built‑in SQLite to Skip Native Builds
Learn how Bun’s built‑in SQLite module removes native‑dependency friction, see a minimal worked example, and weigh the trade‑offs of runtime coupling and synchronous execution.
05 Aug 2025, 13:08 UTC

Why a built‑in SQLite matters
When a project needs a lightweight relational store, reaching for a package like better-sqlite3 brings a native dependency that must be compiled (via node-gyp) or fetched as a pre‑built binary. This step can fail on CI agents, Windows containers, or restricted build environments, adding friction to onboarding and deployment.
How bun:sqlite works
Bun ships a SQLite driver inside the runtime as the bun:sqlite module. Importing it gives you a real embedded database without any extra npm package, no compilation step, and no separate binary download. The API mirrors the synchronous, statement‑oriented style of better-sqlite3: db.query(sql) returns a prepared Statement with methods .get(), .all() and .run(). Prepared statements are automatically cached by the SQL string, so hot‑path queries avoid re‑preparing on each call.
Worked example: a tiny persistence layer
import { Database } from "bun:sqlite";
// In‑memory database for the demo
const db = new Database(":memory:");
// Create a simple table
db.exec(`
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0
);
`);
// Helper to insert a row
function addTask(title) {
const stmt = db.query("INSERT INTO tasks (title) VALUES (?)");
return stmt.run(title); // returns { changes, lastInsertRowid }
}
// Helper to fetch all rows
function listTasks() {
return db.query("SELECT * FROM tasks ORDER BY id").all(); // array of objects
}
// Demo usage
addTask("Learn bun:sqlite");
addTask("Write a CLI tool");
console.log(listTasks());
/* Output:
[ { id: 1, title: 'Learn bun:sqlite', done: 0 },
{ id: 2, title: 'Write a CLI tool', done: 0 } ]
*/
The same code would work with a file‑based database by passing a path to Database (e.g., new Database("data.sqlite")). Pragmas such as journal_mode=WAL can be applied with db.exec() when tuning is needed.
Trade‑offs and limitations
- Runtime coupling: The import
bun:sqliteresolves only inside the Bun runtime. Attempting to run the same file under Node throwsCannot find module 'bun:sqlite'. If you need to stay runtime‑agnostic, wrap the calls behind an abstraction layer or continue usingbetter-sqlite3. - Synchronous API: Each query blocks the event loop while SQLite executes. For short queries this is usually fine, but long‑running operations (e.g., large imports or complex joins) can stall other requests in a single‑process Bun server. Consider off‑loading heavy work to a worker thread or a separate process.
- API evolution: Bun’s SQLite surface has changed across releases (statement caching details, transaction helpers, constructor options). Always check the documentation for the version you pin in
bun.lockborpackage.json.
When to reach for it
For small services, CLI tools, prototypes, or any situation where you want a zero‑dependency embedded store, bun:sqlite removes the native‑build step and simplifies Dockerfiles, CI pipelines, and contributor setup. If you anticipate needing to run the same code under multiple JavaScript runtimes, or if you expect to execute lengthy write‑heavy transactions, evaluate whether the coupling and synchronous nature are acceptable for your workload.
Closing
Bun’s built‑in SQLite module offers a pragmatic way to get a real database with virtually no setup overhead. By understanding its synchronous, runtime‑specific nature, you can decide whether it fits the constraints of your project and enjoy a smoother development experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.