Querying bun:sqlite, Bun's built-in embedded database
Published August 21, 2026
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)`);
const insert = db.query("INSERT INTO users (name, age) VALUES ($name, $age)");
insert.run({ $name: "Ada", $age: 30 });
insert.run({ $name: "Grace", $age: 28 });
const adults = db.query("SELECT name, age FROM users WHERE age >= $min ORDER BY age");
console.log(adults.all({ $min: 18 }));What
bun:sqlite is a native SQLite3 driver built into the Bun binary. new Database(path) opens
a file (or :memory:), db.query(sql) prepares a cached statement, and .run() / .all() /
.get() execute it with bound parameters.
Why it matters
Using SQLite from Node normally means installing better-sqlite3 (native bindings, a
postinstall compile step) or node:sqlite if you're on a new enough Node with an experimental
flag. Bun compiles SQLite directly into the runtime, so import { Database } from "bun:sqlite"
works with no install and no native build step, and Bun's own benchmarks put it several times
faster than better-sqlite3 for reads. The API is also fully synchronous — no await on
queries — because SQLite itself is a synchronous, single-writer engine; pretending otherwise
with a Promise wrapper just adds overhead without buying real concurrency.
How it works
db.exec(...)runs a one-off statement (here,CREATE TABLE) that returns nothing useful to cache.db.query(sql)compiles the SQL once and returns a reusableStatement. Calling.query()again with the same SQL string returns the same cached statement instead of recompiling it.insert.run({ $name, $age })binds named parameters (the$prefix is required unless the database was opened withstrict: true) and executes once per call, safe from SQL injection by construction — values are bound, never string-concatenated into the query.adults.all({ $min: 18 })runs the same prepared statement with different bindings and returns every matching row as an array of plain objects.
Gotchas
- Bindings need the
$,:, or@prefix by default ({ $name: "Ada" }, not{ name: "Ada" }). Passstrict: truetonew Database()to bind without the prefix — and to have Bun throw on a typo'd parameter name instead of silently ignoring it. - Integers past
Number.MAX_SAFE_INTEGERsilently truncate unless the database was opened withsafeIntegers: true, which returns them asbigintinstead. bun:sqliteis synchronous and blocks the event loop for the duration of each query — fine for typical local queries, but a genuinely large scan will stall other work on the same thread.
Related
db.transaction(fn)wraps multiple statements in an atomic transaction — reach for it when inserting many rows, since each one otherwise commits individually.- Bun also supports
import db from "./file.sqlite" with { type: "sqlite" }as a shorthand for opening a file-backed database via an import.