Built-in SQLite
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences
of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; versions and stability levels were
checked against the official documentation on 2026-10-03, and every example below was run on Node.js 26.10.0 and, unless the text says
otherwise, on 24.21.0 and 22.23.3 as well. The node:sqlite module embeds the SQLite engine in the Node.js binary, so a program can store
relational data in a single file (or in memory) with no server, no driver to install and no native build step. It is Stability 1.2 - Release
candidate in the Node.js 26 documentation: it became a release candidate in v25.7.0, it no longer needs a flag since v23.4.0 and v22.13.0,
and it was added in v22.5.0. Release candidate means the shape of the API is considered settled, but it can still change before it becomes
Stable, so this page labels what changed in each line. The module is available only through the node: scheme (import … from 'node:sqlite';
the bare name sqlite does not resolve). The running scenario is the Bookshelf application that grows across this section: this page
gives it a durable store, and Databases from Node.js covers the networked databases it can
graduate to.
The stability and version map
| API | Stability (Node.js 26 docs) | Version notes |
|---|---|---|
|
1.2 Release candidate |
Added in v22.5.0; no flag since v23.4.0 and v22.13.0; release candidate since v25.7.0. Node.js 22 still prints an |
|
Release candidate |
|
|
Release candidate |
|
|
Release candidate |
|
|
Release candidate |
|
|
Release candidate |
Added in v24.10.0; not available on Node.js 22. |
|
Release candidate |
Added in v23.3.0 and v22.12.0. |
|
Release candidate |
Added in v23.8.0 and v22.16.0; returns a promise. |
|
Release candidate |
Added in v26.1.0; not available on 24 or 22. |
|
Release candidate |
Added in v24.9.0; not available on 22. |
|
Release candidate |
Added in v26.8.0. |
|
Release candidate |
|
|
Release candidate |
Added in v23.5.0 and v22.13.0. |
What it is and how Node.js implements it
SQLite is a self-contained, serverless SQL database engine that keeps a whole database in one ordinary file. It runs inside your process as a
library, so there is no network hop, no authentication to configure and no separate service to deploy. Node.js bundles a copy of the C library
(the module wraps functions such as sqlite3_prepare_v3() and sqlite3_step(); the documentation names the C function each method wraps) and
exposes it through C++ bindings to a small JavaScript layer in lib/sqlite.js. The two classes you meet are DatabaseSync, one connection to a
database, and StatementSync, one prepared statement. The Sync suffix is the key design fact: every method runs synchronously on the main
thread and blocks the event loop until SQLite finishes. There is no promise or callback API; the documentation states that all APIs exposed
by DatabaseSync and StatementSync execute synchronously (with one exception, sqlite.backup(), which returns a promise).
That is acceptable because a local file read or write is typically fast, and because an in-process call avoids the per-query latency of a network round trip. It becomes a problem for slow queries or big result sets, for which the page on Don’t Block the Event Loop explains the cost and the section on Worker Threads shows how to move such work to another thread (each thread opens its own connection).
A first program: open, create, insert, query
Open a database with new DatabaseSync(path[, options]). The path is a file path, a Buffer, a URL, or the special name :memory: for a
private in-memory database that disappears when the connection closes. Use exec() to run one or more SQL statements that return no rows
(schema creation, pragmas, scripts read from a file), and prepare() to compile a statement that you will run with parameters.
run(), read with get(), all() and iterate()import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
year INTEGER
) STRICT
`);
const insert = db.prepare('INSERT INTO books (title, author, year) VALUES (?, ?, ?)');
console.log(insert.run('Dune', 'Frank Herbert', 1965));
insert.run('Emma', 'Jane Austen', 1815);
insert.run('Ubik', 'Philip K. Dick', 1969);
const byId = db.prepare('SELECT * FROM books WHERE id = :id');
console.log(byId.get({ id: 1 }));
console.log(byId.get({ id: 99 }));
const recent = db.prepare('SELECT title, year FROM books WHERE year > ? ORDER BY year');
console.log(recent.all(1900));
for (const row of recent.iterate(1900)) console.log('row:', row.title);
recent.setReturnArrays(true);
console.log(recent.all(1900));
console.log(byId.columns().map((c) => c.name));
db.close();
Running it on Node.js 26 prints (the Object: null prototype lines are shortened here):
{ changes: 1, lastInsertRowid: 1 }
{ id: 1, title: 'Dune', author: 'Frank Herbert', year: 1965 }
undefined
[ { title: 'Dune', year: 1965 }, { title: 'Ubik', year: 1969 } ]
row: Dune
row: Ubik
[ [ 'Dune', 1965 ], [ 'Ubik', 1969 ] ]
[ 'id', 'title', 'author', 'year' ]
Source: Node.js API: SQLite (usage example and StatementSync methods)
The four execution methods differ only in what they hand back:
| Method | Returns | Use it for |
|---|---|---|
|
|
|
|
The first row as an object, or |
Lookups by key, |
|
An array of row objects (empty when no rows match) |
Small, bounded result sets. |
|
An iterable iterator of row objects |
Large result sets you want to process one row at a time; stop early with |
Two behaviours surprise newcomers. Rows are objects with a null prototype (observed in the output above), so they have no toString() and
row.hasOwnProperty does not exist; use Object.hasOwn(row, 'title') or spread them into a normal object. And get() returns undefined
for no row while a NULL column is read as null, never undefined.
Binding parameters and type conversion
Always pass values as parameters, never by building SQL text from them. Parameters are either anonymous (?, bound in order from the
arguments of run(), get(), all() or iterate(); ?NNN assigns an explicit index) or named ($name, :name or @name, bound from an
object passed as the first argument). By default node:sqlite allows bare names in the object: { id: 1 } binds :id. Binding a key that
the statement does not use throws ERR_INVALID_STATE unless unknown names are allowed (allowUnknownNamedParameters option or
statement.setAllowUnknownNamedParameters(true)); using both $k and @k in one statement makes a bare name ambiguous and throws.
| SQLite | JavaScript to SQLite | SQLite to JavaScript |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
An INTEGER outside the safe-integer range cannot be returned as a number: reading it throws ERR_OUT_OF_RANGE unless you ask for BigInts.
Any other JavaScript type (an object, a Date, a Map) throws ERR_INVALID_ARG_TYPE; convert it first (store dates as ISO-8601 text or epoch
integers, objects as JSON text).
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t (n INTEGER)');
db.prepare('INSERT INTO t VALUES (?)').run(9007199254740993n);
try {
db.prepare('SELECT n FROM t').get();
} catch (e) {
console.log(e.code); // ERR_OUT_OF_RANGE
}
const s = db.prepare('SELECT n FROM t');
s.setReadBigInts(true);
console.log(s.get()); // { n: 9007199254740993n }
try {
db.prepare('SELECT $a').get({ b: 1 });
} catch (e) {
console.log(e.code, e.message); // ERR_INVALID_STATE Unknown named parameter 'b'
}
Source: Node.js API: SQLite (type conversion and binding parameters)
Binding undefined changed recently. In Node.js 26.10.0 it binds NULL; on Node.js 24.21.0 and 22.23.3 get(undefined) throws a
TypeError with code ERR_INVALID_ARG_TYPE (observed). Booleans as parameters are documented as added in v26.8.0, so on older lines convert
them to 1 and 0 yourself.
Parameterised queries and SQL injection
A prepared statement separates the SQL text, which SQLite compiles once, from the values, which are bound afterwards and are never parsed as SQL. That is what makes a hostile string harmless. The mistake to avoid is string interpolation into the SQL text:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT NOT NULL)');
db.exec("INSERT INTO books (title) VALUES ('Dune'), ('Emma'), ('Ubik')");
const title = "x' OR '1'='1"; // untrusted input, e.g. from a query string
// WRONG: the value becomes part of the SQL text, so it rewrites the query
console.log(db.prepare(`SELECT * FROM books WHERE title = '${title}'`).all());
// RIGHT: the value travels separately as a bound parameter
console.log(db.prepare('SELECT * FROM books WHERE title = ?').all(title));
Output on Node.js 26, 24 and 22 (identical): the interpolated query returns every row, the bound one matches no title.
[
[Object: null prototype] { id: 1, title: 'Dune' },
[Object: null prototype] { id: 2, title: 'Emma' },
[Object: null prototype] { id: 3, title: 'Ubik' }
]
[]
Source: Node.js API: statement.all()
Values can be parameters; identifiers cannot. A table or column name chosen at run time (for example a sort column) must be checked against an
allow-list in your code before it goes into the SQL text. The sqlite.db.query diagnostics channel and statement.expandedSQL show the SQL
with values substituted in, which is for debugging only; do not execute that string.
Transactions
node:sqlite has no transaction() helper: you drive transactions with plain SQL through exec(). BEGIN opens one, COMMIT makes it
durable, ROLLBACK discards it, and database.isTransaction reports whether you are inside one. Without an explicit transaction, every
run() is its own implicit transaction, which for file databases means a disk sync per statement: wrapping a batch of inserts in one
transaction is the single biggest performance win. BEGIN IMMEDIATE takes the write lock up front, which avoids a later SQLITE_BUSY error
when a read transaction tries to upgrade to a write.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE loans (id INTEGER PRIMARY KEY, book TEXT NOT NULL UNIQUE, member TEXT NOT NULL)');
const loan = db.prepare('INSERT INTO loans (book, member) VALUES (?, ?)');
function transaction(fn) {
db.exec('BEGIN IMMEDIATE');
try {
const result = fn();
db.exec('COMMIT');
return result;
} catch (err) {
if (db.isTransaction) db.exec('ROLLBACK');
throw err;
}
}
transaction(() => {
loan.run('Dune', 'ana');
loan.run('Emma', 'ben');
});
try {
transaction(() => {
loan.run('Ubik', 'ana');
loan.run('Dune', 'cleo'); // violates UNIQUE: the whole transaction rolls back
});
} catch (err) {
console.log(err.code, err.errcode, err.errstr);
}
console.log(db.prepare('SELECT count(*) AS n FROM loans').get());
console.log(db.isTransaction);
ERR_SQLITE_ERROR 2067 constraint failed
[Object: null prototype] { n: 2 }
false
Source: Node.js API: SQLite (database.exec(), database.isTransaction)
SQLite failures surface as an Error with code ERR_SQLITE_ERROR plus the SQLite numeric result code (errcode, here 2067, a unique
constraint violation) and its text (errstr); these two properties are what the run above shows (observed, not described in the API page, so
treat them as informational and branch on them with care). Because the API is synchronous, a transaction cannot be interleaved with another
await`ed task on the same connection by accident: as long as the callback passed to a helper like the one above is synchronous, nothing else
in your process can run between `BEGIN and COMMIT. Do not put an await inside such a callback.
Configuring the connection
The constructor options (all Release candidate) set defaults for every statement of the connection:
| Option | Default | Effect |
|---|---|---|
|
|
When |
|
|
Opens read-only; the database must already exist. Writes fail with |
|
|
Foreign keys are enforced (SQLite itself defaults to off, node:sqlite turns it on). |
|
|
Accepts |
|
|
Enables |
|
|
Busy timeout in milliseconds: how long SQLite waits for a lock before failing. Set a few seconds for a file shared by several connections. |
|
|
Defaults for statements: BigInt integers; rows as arrays instead of objects. |
|
|
Named-parameter strictness described above. |
|
|
Disables SQL features that can deliberately corrupt the file. On by default since v25.5.0 and v24.14.0 (the option was added in v25.1.0 and v24.12.0). |
|
SQLite defaults |
Caps on string length, SQL length, columns, expression depth and more, useful when the SQL or data is not fully trusted. |
SQLite settings that are not constructor options go through PRAGMA statements with exec(). Two matter for almost every file-backed
database. PRAGMA journal_mode = WAL switches to write-ahead logging, in which readers do not block the writer and the writer does not
block readers; the mode persists in the file, and it does not apply to :memory: databases (the pragma reports memory, observed). WAL
needs a filesystem with shared memory, so it is unsuitable for databases on network filesystems. PRAGMA synchronous = NORMAL is the usual
companion setting in WAL mode. PRAGMA user_version is a free integer slot in the file header that is handy for schema versions, shown next.
user_versionimport { DatabaseSync } from 'node:sqlite';
const migrations = [
'CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL) STRICT',
'ALTER TABLE books ADD COLUMN year INTEGER',
'CREATE INDEX books_author ON books (author)',
];
export function openBookshelf(path) {
const db = new DatabaseSync(path, { timeout: 5000 });
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA synchronous = NORMAL');
const { user_version: current } = db.prepare('PRAGMA user_version').get();
for (let v = current; v < migrations.length; v++) {
db.exec('BEGIN');
try {
db.exec(migrations[v]);
db.exec(`PRAGMA user_version = ${v + 1}`); // v is our own loop counter, not user input
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
}
return db;
}
const db = openBookshelf(':memory:');
console.log(db.prepare('PRAGMA user_version').get());
[Object: null prototype] { user_version: 3 }
Calling the function twice on a file path applies each migration exactly once, because the stored user_version is read first. For anything
beyond a handful of migrations use a migration tool; see Choosing a Migration Tool.
Tagged statements and the statement cache
Calling prepare() for every query wastes the compile step, and keeping a map of prepared statements by hand is boilerplate. database.createTagStore([maxSize])
(since v24.9.0) returns an SQLTagStore: a least-recently-used cache of prepared statements, keyed by the SQL text, which you use as a
template-literal tag. The ${value} slots become bound parameters, not string interpolation. The store has run, get, all and iterate
tags, size, capacity (default 1000 statements), db and clear().
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, year INTEGER)');
const sql = db.createTagStore(100);
sql.run`INSERT INTO books (title, year) VALUES (${'Dune'}, ${1965})`;
sql.run`INSERT INTO books (title, year) VALUES (${'Emma'}, ${1815})`;
const minYear = 1900;
console.log(sql.all`SELECT title FROM books WHERE year > ${minYear}`);
console.log(sql.get`SELECT count(*) AS n FROM books`);
console.log(sql.size, sql.capacity); // 3 100
const evil = "x'; DROP TABLE books; --";
console.log(sql.get`SELECT * FROM books WHERE title = ${evil}`); // undefined: it is just data
console.log(sql.get`SELECT count(*) AS n FROM books`); // { n: 2 }
Source: Node.js API: SQLite (database.createTagStore(), SQLTagStore)
The cache matches on the exact SQL text (case-sensitive, with placeholders in the same positions): a query that writes a literal 12345
where another used a slot is a different statement. Never put ? placeholders inside a tagged template, and never build the SQL
with an untagged template literal passed to prepare(), where a substitution is plain string interpolation and therefore an injection hole.
Resource management: close, using and statement lifetime
Statements and connections hold native memory. Close a connection with database.close() (it throws if the database is already closed or
if you call it from inside a user function, authorizer or query subscriber), or rely on using, which calls database[Symbol.dispose]() (a no-op when
already closed). The explicit resource management syntax (using) needs a V8 that supports it: it parsed on Node.js 24 and 26 and was a
SyntaxError on 22.23.3 (observed). Statements gained close() and Symbol.dispose only in v26.8.0 (on 24 a using statement fails
with Symbol(Symbol.dispose) is not a function, observed); earlier lines finalise a statement when it is garbage collected.
import { DatabaseSync } from 'node:sqlite';
{
using db = new DatabaseSync(':memory:');
using stmt = db.prepare('SELECT 1 AS one'); // statement disposal: Node.js 26.8 and later
console.log(stmt.get()); // { one: 1 }
} // stmt, then db, are closed here
Source: Node.js API: SQLite (database[Symbol.dispose](), statement.close())
User-defined functions, aggregates and the authorizer
SQLite can call back into JavaScript. database.function(name[, options], fn) registers a scalar SQL function; database.aggregate(name, options)
registers an aggregate (and, when you also give inverse, a window function) with start, step and an optional result. Options such as
deterministic (the function always returns the same output for the same input, which lets SQLite optimise), directOnly (not callable from
triggers or views), useBigIntArguments and varargs map to SQLite function flags. The return value must be a valid SQLite value (see the
type table), and undefined becomes NULL.
database.setAuthorizer(callback) (since v24.10.0) installs a callback that SQLite invokes whenever a statement is compiled and wants to
read, write or change the schema; it returns SQLITE_OK, SQLITE_DENY (error) or SQLITE_IGNORE. It is a defence-in-depth tool for running
partly trusted SQL. The authorizer must not touch the connection that invoked it (calling prepare() or exec() from inside it throws
ERR_INVALID_STATE).
DELETEimport { DatabaseSync, constants } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec("CREATE TABLE books (title TEXT, author TEXT); INSERT INTO books VALUES ('Dune','Frank Herbert'),('Ubik','Philip K. Dick')");
db.function('slug', { deterministic: true }, (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
console.log(db.prepare('SELECT slug(title) AS slug FROM books').all()); // dune, ubik
db.aggregate('longest', {
start: '',
step: (acc, value) => (value.length > acc.length ? value : acc),
});
console.log(db.prepare('SELECT longest(author) AS a FROM books').get()); // { a: 'Philip K. Dick' }
db.setAuthorizer((action) =>
action === constants.SQLITE_DELETE ? constants.SQLITE_DENY : constants.SQLITE_OK);
try {
db.exec('DELETE FROM books');
} catch (e) {
console.log(e.code, e.message); // ERR_SQLITE_ERROR not authorized
}
Source: Node.js API: SQLite (database.function(), database.aggregate(), database.setAuthorizer())
On Node.js 22 the scalar function example works (the function() method was added in v22.13.0, aggregate() in v22.16.0), but
setAuthorizer() does not exist before v24.10.0.
Sessions and changesets
The SQLite session extension records the changes made to a database so they can be replayed on another one. database.createSession([options])
(since v23.3.0 and v22.12.0) returns a Session that tracks all tables, or only options.table. session.changeset() returns a Uint8Array with
every change since the session was created (it can be called repeatedly); session.patchset() returns a more compact form that omits the
old values. target.applyChangeset(changeset[, options]) replays it and returns true when it was applied without being aborted. Two
options control it: filter(table) decides which tables to apply, and onConflict(type) must return SQLITE_CHANGESET_OMIT (skip),
SQLITE_CHANGESET_REPLACE (overwrite; only valid for data and primary-key conflicts) or SQLITE_CHANGESET_ABORT (roll back, the default).
Typical uses are shipping a delta to a replica, offline-first synchronisation and auditing what a migration changed.
import { DatabaseSync, constants } from 'node:sqlite';
const primary = new DatabaseSync(':memory:');
const replica = new DatabaseSync(':memory:');
for (const db of [primary, replica]) {
db.exec('CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT)');
}
const session = primary.createSession({ table: 'books' });
const insert = primary.prepare('INSERT INTO books (title) VALUES (?)');
insert.run('Dune');
insert.run('Emma');
const changeset = session.changeset(); // Uint8Array
const ok = replica.applyChangeset(changeset, {
filter: (table) => table === 'books',
onConflict: () => constants.SQLITE_CHANGESET_OMIT,
});
console.log(ok, replica.prepare('SELECT * FROM books').all()); // true, both rows
// Applying the same changeset again conflicts on the primary keys; OMIT skips them
console.log(replica.applyChangeset(changeset, { onConflict: () => constants.SQLITE_CHANGESET_OMIT })); // true
session.close();
Source: Node.js API: SQLite (database.createSession(), database.applyChangeset()), SQLite: Session Extension
Both databases need the table to exist with the same schema; the changeset carries row changes, not DDL. session.close() releases the
session (the session is also disposable since v24.9.0).
Backup, serialize and deserialize
sqlite.backup(sourceDb, path[, options]) (since v23.8.0 and v22.16.0, a function exported from the module, not a method) copies a live
database to a file. It is the one asynchronous API of the module: it returns a promise that fulfils with the number of pages copied, so the
event loop gets a turn between batches. options.rate is the number of pages per step (default 100), options.progress is called after
each step with { totalPages, remainingPages }, and options.source and options.target name the attached databases (default 'main'). The
source stays usable while the backup runs: writes from the same connection are reflected right away, whereas writes from another
connection restart the backup. An existing destination file is overwritten.
import { DatabaseSync, backup } from 'node:sqlite';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const dir = mkdtempSync(join(tmpdir(), 'bookshelf-'));
try {
const db = new DatabaseSync(join(dir, 'bookshelf.db'));
db.exec('PRAGMA journal_mode = WAL');
db.exec('CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT)');
const ins = db.prepare('INSERT INTO books (title) VALUES (?)');
db.exec('BEGIN');
for (let i = 0; i < 50000; i++) ins.run(`Book ${i}`);
db.exec('COMMIT');
let steps = 0;
const pages = await backup(db, join(dir, 'copy.db'), {
rate: 5, // pages per step: small, so the progress callback fires
progress: ({ totalPages, remainingPages }) => {
steps++;
},
});
console.log('pages copied:', pages > 0, 'progress calls:', steps > 0);
const copy = new DatabaseSync(join(dir, 'copy.db'), { readOnly: true });
console.log(copy.prepare('SELECT count(*) AS n FROM books').get()); // { n: 50000 }
try {
copy.exec('DELETE FROM books');
} catch (e) {
console.log(e.code, e.message); // ERR_SQLITE_ERROR attempt to write a readonly database
}
db.close();
copy.close();
} finally {
rmSync(dir, { recursive: true, force: true });
}
Source: Node.js API: SQLite (sqlite.backup()), Node.js API: File system (mkdtempSync, rmSync)
On all three lines the run printed pages copied: true progress calls: true. The progress callback is called after each backup step, so a
small database that fits in one step may produce no progress calls at all (observed with 2000 rows and rate: 50); do not rely on an exact
count. For an in-memory snapshot,
database.serialize([dbName]) returns the whole database as a Uint8Array and database.deserialize(buffer[, options]) loads it into
another connection (both since v26.1.0; neither exists on Node.js 24 or 22). Serialising a database that is in WAL mode and loading it
into an in-memory connection failed with unable to open database file in this page’s trial (observed), so serialise databases that use the
default journal mode, or use backup() for file databases.
import { DatabaseSync } from 'node:sqlite';
const mem = new DatabaseSync(':memory:');
mem.exec("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('hello')");
const bytes = mem.serialize();
const clone = new DatabaseSync(':memory:');
clone.deserialize(bytes);
console.log(bytes instanceof Uint8Array, clone.prepare('SELECT v FROM t').get()); // true { v: 'hello' }
Source: Node.js API: SQLite (database.serialize(), database.deserialize())
Observing and limiting what SQLite does
Several smaller APIs help in production:
-
Tracing. The documentation says SQL trace events can be observed through the
diagnostics_channelmodule on thesqlite.db.querychannel. In a trial on Node.js 26 a subscriber received messages withsql,databaseanddurationfields (observed; read the diagnostics_channel page for the contract), and it received nothing on 24. -
Statement counters (Node.js 26.8 and later).
statement.stat(counter)reads SQLite’s per-statement counters:'fullscanStep','sort','autoindex','vmStep','reprepare','run','filterMiss','filterHit'and'memused';statement.resetStats()zeroes them. Assertingstat('fullscanStep') === 0in a test is a cheap guard against a query that silently stopped using its index. -
Limits and extensions.
database.limitsand thelimitsconstructor option cap resource use.allowExtension: trueenablesloadExtension(path), which loads a shared library into the process and runs its native code with your privileges: enable it only for libraries you trust, and callenableLoadExtension(false)once you have loaded what you need (when the constructor option isfalse, extension loading cannot be enabled later). -
Column metadata.
statement.columns()returnsname,column,table,databaseandtypefor each result column, useful to build generic table viewers or typed mappers.
One security observation: the Node.js permission model does not appear to gate SQLite’s own file access. In a trial on Node.js 26 with
--permission --allow-fs-read set to the working directory and no --allow-fs-write, new DatabaseSync('./perm.db') still created the file and
a table (observed; the JavaScript node:fs APIs are what --allow-fs-write governs, because SQLite opens the file itself in C). Do not
treat the permission flags as a sandbox for database writes; restrict the process with operating-system or container permissions as well. See
Permission Model for what the flags do cover.
Testing with SQLite
An in-memory database is the cheapest test fixture the platform offers: each test opens new DatabaseSync(':memory:'), runs the same
migration function as production and is isolated from every other test, with no cleanup. The Bookshelf openBookshelf(':memory:') above is
exactly that. The page on the Node.js test runner shows the node:test structure around it. In-memory
databases do not support WAL and cannot be shared between connections, so tests that exercise concurrent access should use a temporary file
instead.
When SQLite is the right tool, and when to move to a server database
SQLite is not a toy. Its own documentation lists embedded applications, devices, local data stores, small to medium websites and data-analysis files as good fits, and says client/server databases fit better for several situations. Use these criteria for a Node.js service:
| Question | Stay on SQLite when | Move to PostgreSQL (or another server database) when |
|---|---|---|
How many processes write? |
One process owns the file (a single Node.js process or a single worker thread doing the writes). WAL gives concurrent readers. |
Several machines or many processes need to write at once; SQLite allows one writer at a time per database file. |
Where is the data? |
On a local disk of one machine (a CLI, a desktop tool, a single container with a volume). |
The database must be reachable over the network, or the application scales horizontally across hosts; SQLite on a network filesystem is unsafe. |
How big and how busy? |
Up to modest sizes and request rates; reads dominate. |
Heavy concurrent write load, multi-terabyte data, or workloads needing replication, fine-grained roles and online failover. |
Does the loop stay free? |
Queries are short and indexed; the blocking call is microseconds. |
Slow analytical queries or large result sets would block the event loop; run them in a worker thread or on a server database. |
What features are needed? |
Plain SQL, JSON functions, transactions, a single-file backup. |
Rich types, full-text and geospatial extensions, row-level security, logical replication, a managed service. |
A path that works well is to start the Bookshelf on node:sqlite (prototypes, tests, CLI tools, single-instance deployments) and keep the data
access behind a small module, so that swapping to PostgreSQL later changes one file. The SQL dialect differences are small for the simple
queries used here, but not zero (type names, AUTOINCREMENT versus identity columns, ? versus $1 placeholders). The next page,
Databases from Node.js, covers PostgreSQL, MongoDB and Redis clients, connection pools and
the ORM landscape; for SQL itself see the SQL reference and Choosing the Right Database.
Pitfalls summary
| Mistake | Consequence and fix |
|---|---|
Building SQL with template-literal interpolation |
SQL injection; bind parameters ( |
Running a slow query on the main thread |
Every other request waits; index the query, page the results with |
One transaction per row for bulk inserts |
A disk sync per row; wrap the batch in |
An |
Other tasks run while the transaction is open and a failure leaves it dangling; keep the section synchronous. |
Reading an integer above 2 to the power 53 without |
|
Assuming |
Missing on Node.js 22 (and 24 for some); check the version map above. |
Opening the same file from several processes without |
|
Treating the module as Stable |
The API is a release candidate: pin your Node.js version and read the release notes when upgrading. |
What changed recently
| Version | Change |
|---|---|
v22.5.0 |
Module added behind |
v23.4.0, v22.13.0 |
No longer behind the flag; |
v24.0.0, v22.16.0 |
|
v24.2.0 |
|
v24.9.0 |
|
v24.10.0 |
|
v25.1.0, v24.12.0 and v25.5.0, v24.14.0 |
|
v25.7.0 |
The module becomes a release candidate. |
v25.8.0 |
|
v26.1.0 |
|
v26.8.0 |
|
v26.10.0 |
|
Where the books are outdated
Node.js Projects (Jonathan Wexler) uses SQLite through Sequelize in its library-API project and again in its AI-assistant project, which
means installing a third-party package and an ORM; for a prototype, a small service or a test fixture the built-in module removes both, at the price
of writing SQL yourself and of a release-candidate API. Neither book covers node:sqlite, which was added in v22.5.0 and only lost its flag in
v23.4.0 and v22.13.0, so treat any recommendation in them to install a separate SQLite package as dated for simple cases. Third-party drivers such as better-sqlite3 remain valid choices: they are stable, support additional features and run on older Node.js
lines, which can matter if you cannot yet depend on a release-candidate core module.
References
-
Node.js API: SQLite, https://nodejs.org/docs/latest/api/sqlite.html
-
Node.js API: statement.all(), https://nodejs.org/docs/latest/api/sqlite.html#statementallnamedparameters-anonymousparameters
-
Node.js API: Diagnostics Channel, https://nodejs.org/docs/latest/api/diagnostics_channel.html
-
Node.js API: File system, https://nodejs.org/docs/latest/api/fs.html
-
Node.js API: Worker threads, https://nodejs.org/docs/latest/api/worker_threads.html
-
Node.js API: Test runner, https://nodejs.org/docs/latest/api/test.html
-
Node.js 22.5.0 release notes, https://nodejs.org/en/blog/release/v22.5.0
-
SQLite: Appropriate uses for SQLite, https://www.sqlite.org/whentouse.html
-
SQLite: Write-ahead logging, https://www.sqlite.org/wal.html
-
SQLite: PRAGMA statements, https://www.sqlite.org/pragma.html
-
SQLite: Session extension, https://www.sqlite.org/sessionintro.html
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/