10LOC
Bunintermediate

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 reusable Statement. 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 with strict: 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" }). Pass strict: true to new 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_INTEGER silently truncate unless the database was opened with safeIntegers: true, which returns them as bigint instead.
  • bun:sqlite is 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.