better-sqlite3 review
better-sqlite3 embeds SQLite in a Node process and gives you a synchronous Database and Statement API. Prepared statements expose get(), all(), run(), and iterate(); transactions commit on return and roll back when the wrapped function throws. The package also covers online backup, serialization, custom SQL functions, aggregates, virtual tables, extensions, and lossless 64-bit integers. Version 13 moved the native addon to N-API, puts prebuilt binaries in the npm package, requires Node 22 or newer, and adds db.explain() plus Statement.toString(). Release 13.0.3 changes the ARM build job to Ubuntu 22.04.
Install better-sqlite3 for a Node 22 application that owns a local SQLite file and benefits from direct, synchronous SQL. Walk away when slow queries, many writers, browser execution, or built-in query typing are part of the requirement.
We installed it
| Install | ✓ · 0.7s | 2 packages on disk · 27 MB · native build step |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does better-sqlite3 install cleanly?
Yes. In a fresh container with an empty cache, npm install better-sqlite3 finished in 0.7s, leaving 2 packages and 27 MB on disk, after a native build step. npm audit reported no known vulnerabilities.
Can better-sqlite3 run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does better-sqlite3 work with both ESM and CommonJS?
Yes. Both import 'better-sqlite3' and require('better-sqlite3') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does better-sqlite3 include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
better-sqlite3 or sqlite3: which should you use?
sqlite3: Choose it when an asynchronous callback-oriented SQLite driver fits existing code better than a synchronous one. Install better-sqlite3 for a Node 22 application that owns a local SQLite file and benefits from direct, synchronous SQL.
When should you not use better-sqlite3?
A report or analytical query can occupy the event loop for a noticeable time. The API is synchronous, and the project sends slow-query workloads to worker threads.
Use it if
- Your application owns one local SQLite file and normal queries finish quickly enough to run on Node's main thread
- You want prepared SQL and transactions without callbacks, promises, an ORM, or a migration layer
- You need SQLite features exposed from JavaScript, including custom functions, aggregates, read-only virtual tables, online backup, and extension loading
- You store integers above JavaScript's safe Number range and can opt into BigInt results with safeIntegers()
- A report or analytical query can occupy the event loop for a noticeable time. The API is synchronous, and the project sends slow-query workloads to worker threads.
- Your service has many concurrent writers or runs across autoscaled containers. SQLite serializes writes, and a locked connection eventually raises SQLITE_BUSY after the configured timeout.
- You deploy on Node 20 or older. Version 13 declares Node >=22, so the current release will not install within that supported engine range.
- You need browser or edge-runtime code. Our browser bundle failed, and the package contains a native Node addon rather than portable browser JavaScript.
- You expect typed query results, schema migrations, or a query builder from the driver. The package ships no TypeScript declarations and deliberately stays at the SQLite API layer.
Setup reality
Our clean Node 22 install of 13.0.3 succeeded in 0.7 seconds. It left 2 packages using 27 MB, ran a native or compile step, and npm audit found 0 known vulnerabilities. The package has 1 direct dependency and no peer dependencies. CommonJS require() and ESM import both worked. No TypeScript types were present, and an esbuild browser bundle failed because this is Node-only native code.
Version 13 requires Node 22 or newer. Its N-API rewrite places prebuilt binaries inside the published package instead of fetching them through prebuild-install. An unsupported platform or architecture can still fall through to compilation, so that machine needs the node-gyp prerequisites. Bundlers must leave the native addon external and preserve the path to its .node binary.
Open the file once, then set connection behavior deliberately. The README recommends WAL mode for concurrent reads and writes. In WAL mode this build defaults synchronous to NORMAL; choose FULL if your durability requirements demand it. The constructor waits 5000 ms for a locked database unless you change timeout, then SQLite reports SQLITE_BUSY. Transactions cannot wrap async functions because the wrapper commits when that function first returns a promise.
Long reads can keep a WAL checkpoint from completing, allowing the WAL file to grow. Slow queries also block the Node thread that owns the connection. Put those queries and their database connection in a worker thread. Online backup is asynchronous, but a write from another connection restarts its copy, so the API documentation recommends one writing connection while a backup runs.
Patterns
Open a database and enable WAL open-database
import Database from 'better-sqlite3';
const db = new Database('app.db', { timeout: 5000 });
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');WAL improves read and write concurrency. foreign_keys is connection state, so set it whenever you open a connection.
Read one row with a prepared statement read-one-row
const findUser = db.prepare(
'SELECT id, email FROM users WHERE id = ?'
);
const user = findUser.get(42);get() returns undefined when the query produces no row. Reuse the prepared Statement instead of preparing inside a loop.
Read a bounded list read-many-rows
const recent = db.prepare(`
SELECT id, title
FROM posts
ORDER BY created_at DESC
LIMIT ?
`).all(20);all() builds the complete result array before returning. Use iterate() when the selected result is too large to hold at once.
Insert with named parameters insert-named-values
const insert = db.prepare(`
INSERT INTO users (email, active)
VALUES (@email, @active)
`);
const result = insert.run({ email: 'ada@example.com', active: 1 });
console.log(result.changes, result.lastInsertRowid);Object keys omit the @, : or $ prefix used in the SQL placeholder.
Insert a batch in one transaction batch-transaction
const insertEvent = db.prepare(
'INSERT INTO events (kind, payload) VALUES (?, ?)'
);
const insertBatch = db.transaction((events) => {
for (const event of events) {
insertEvent.run(event.kind, JSON.stringify(event.payload));
}
});
insertBatch(events);The wrapped function must stay synchronous. An async function commits before work after its first await runs.
Process rows without building an array iterate-results
const rows = db.prepare(
'SELECT id, payload FROM events ORDER BY id'
).iterate();
for (const row of rows) {
processEvent(row);
}iterate() keeps the statement busy while the iterator is open. Finish or break from the loop before reusing it.
Return SQLite integers as BigInt preserve-integers
const count = db.prepare(
'SELECT COUNT(*) FROM events'
).safeIntegers().pluck().get();
console.log(typeof count, count);BigInt preserves values outside Number's safe range, but JSON.stringify cannot encode BigInt without a replacer.
Handle a unique constraint handle-constraint-error
import Database, { SqliteError } from 'better-sqlite3';
try {
insert.run({ email, active: 1 });
} catch (error) {
if (error instanceof SqliteError && error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return { ok: false, reason: 'email-exists' };
}
throw error;
}Match the extended result code rather than SQLite's message text.
Expose a JavaScript function to SQL define-sql-function
db.function('lower_ascii', { deterministic: true }, (value) =>
String(value).toLowerCase()
);
const row = db.prepare(
'SELECT id FROM users WHERE lower_ascii(email) = ?'
).get(input.toLowerCase());The callback runs inside the synchronous query. Keep it quick and mark it deterministic only when equal inputs always produce equal outputs.
Inspect an index choice inspect-query-plan
const plan = db.explain(
'QUERY PLAN SELECT * FROM users WHERE email = ?'
);
console.table(plan);db.explain() was added in version 13 and does not require placeholder values. QUERY PLAN returns the concise planner rows.
Back up an open database backup-live-database
await db.backup('backup.db', {
progress({ totalPages, remainingPages }) {
console.log({ totalPages, remainingPages });
return 100;
},
});A mutation from a different connection restarts the backup. Closing the source connection aborts pending backups.
Close on process shutdown close-connection
function shutdown() {
if (db.open) db.close();
}
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);close() throws if a transaction is open, a statement iterator is active, or a backup is still running. Stop that work before shutdown.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sqlite3 | npm | Choose it when an asynchronous callback-oriented SQLite driver fits existing code better than a synchronous one. |
| @libsql/client | npm | Choose it for an async client that can talk to libSQL over a remote URL as well as work with local files. |
| drizzle-orm | npm | Choose it when typed schema definitions, query construction, and migrations matter more than using the driver directly. |
| kysely | npm | Choose it when you want a typed SQL query builder and are willing to configure a separate SQLite dialect and driver. |
More data guides
numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

