mrkeyoor.com_
Mon 21 Sept 21:52 UTC
npmDataupdated 21 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed better-sqlite3Screenshot of better-sqlite3 documentation
Install✓ · 0.7s2 packages on disk · 27 MB · native build step
ImportESM import works · require() works · CommonJS package with exports map
Browsern/acould not be bundled for the browser (Node-only code, most likely)
Typesno TypeScript types found
Known vulns00 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.

API stability4/5Database, Statement, prepared-query methods, and transaction wrappers are documented as a small direct API, and version 13 adds db.explain() and Statement.toString() without replacing the core query surface. The current major does change deployment assumptions: it raises the engine floor to Node 22 and rewrites the native layer around N-API, so major upgrades still need runtime and packaging tests.
Docs4/5The repository has separate API, performance, worker-thread, integer, compilation, and unsafe-mode guides. The API reference explains defaults such as the 5000 ms lock timeout, why transaction wrappers cannot accept async functions, and how external writes restart online backups. The material is detailed, though it is split across repository Markdown pages rather than a searchable documentation site.
Maintenance4/5Release 13.0.3 was published on 2026-08-05, and GitHub records a repository push on 2026-08-10. The latest patch adjusts the ARM build environment, while 13.0.0 delivered the N-API port and two API additions. GitHub currently reports 70 open issues and pull requests together, which shows active traffic but should not be read as an issue-only backlog.
Ecosystem5/5npm recorded 10,129,118 downloads in the measured week, and the GitHub repository has 7,452 stars. Its direct Statement interface is also a common target for higher-level packages such as Drizzle and Kysely adapters. That reach gives teams examples and integration choices, while the package itself remains a focused native SQLite driver with a single runtime dependency.

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()
Skip it if

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

PackageRegistryPick it when
sqlite3npmChoose it when an asynchronous callback-oriented SQLite driver fits existing code better than a synchronous one.
@libsql/clientnpmChoose it for an async client that can talk to libSQL over a remote URL as well as work with local files.
drizzle-ormnpmChoose it when typed schema definitions, query construction, and migrations matter more than using the driver directly.
kyselynpmChoose 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.