Node.js 24 Built-In SQLite: Replacing better-sqlite3 in Scripts and Edge Workers Without a Native Module
Node.js 24 ships with native SQLite support. Understand when the built-in node:sqlite module can replace better-sqlite3 in production scripts and edge workers, and when the native module still wins.
Most SQLite integration problems in Node.js stem from the gap between needing a simple embedded database and the friction of compiling native modules. Teams reach for better-sqlite3 because it solves the synchronous API problem, but the build step introduces deployment headaches. Cross-compilation breaks. Docker images balloon. Edge worker environments reject native binaries outright.
Node.js 24 ships with node:sqlite, a built-in module that exposes SQLite without requiring a native dependency. The module runs queries, manages transactions, and handles schema migrations using the same synchronous patterns developers already rely on.
flowchart LR
A("Developer writes script") --> B("Requires better-sqlite3")
B --> C("npm install fails in CI")
style C stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
The built-in module eliminates the compilation step. Teams write scripts that run immediately on any Node.js 24 runtime without a build phase. Edge workers accept the code because the SQLite engine ships inside the Node.js binary itself.
flowchart LR
A("Developer writes script") --> B("Imports node:sqlite")
B --> C("Runs on any Node.js 24 runtime")
style C stroke:#34d399,fill:#0b3b2e,color:#d1fae5
This matters because deployment complexity compounds. A single native dependency multiplies into platform-specific builds, architecture matrices, and runtime version locks. The built-in module collapses that matrix to zero.
Key Takeaways
- Node.js 24 includes
node:sqlite, a native SQLite module that requires no compilation or external dependencies. - The built-in module provides synchronous query execution and transaction support, matching the core API patterns of better-sqlite3.
- Scripts and edge workers gain immediate compatibility because the SQLite engine ships inside the Node.js binary.
- Feature gaps exist: better-sqlite3 offers user-defined functions, custom aggregates, and fine-grained performance tuning that
node:sqlitedoes not yet support. - Migration paths depend on whether the codebase relies on better-sqlite3's advanced hooks or simply needs basic CRUD and transaction semantics.
The Legacy Landscape: Why better-sqlite3 Dominated
Developers adopted better-sqlite3 because the alternative, node-sqlite3, forced asynchronous APIs onto a database that operates synchronously.
SQLite runs on the same thread as the caller. Every query blocks until completion. Wrapping that in Promises or callbacks adds overhead without architectural benefit. The mismatch shows up in transaction code. An asynchronous wrapper turns a three-statement atomic operation into callback nesting or async/await ceremony that obscures the transactional boundary.
better-sqlite3 exposed the synchronous reality directly. Transactions became readable. Statements returned results immediately. The library compiled a native module, but the API aligned with how SQLite actually works.
flowchart TD
A("SQLite engine operates synchronously") --> B("node-sqlite3 wraps in callbacks")
A --> C("better-sqlite3 exposes sync API")
B --> D("Transaction code becomes nested callbacks")
C --> E("Transaction code is linear and readable")
style D stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The compilation requirement created the only friction point. Teams running CI pipelines hit build failures when Python or build-essential packages were missing. Dockerfile layers grew to accommodate the toolchain. Edge environments like Cloudflare Workers rejected the native binary outright.
That tradeoff made sense when no alternative existed. The API clarity justified the deployment cost. Node.js 24 changes the equation by offering synchronous SQLite access without the native module penalty.
node:sqlite API Deep Dive: Opening Databases and Running Queries
The node:sqlite module ships as part of the Node.js standard library. Import it the same way as node:fs or node:path.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');The constructor accepts a file path or the special :memory: token for in-memory databases. No options object. No compilation step. The database opens immediately.
Running queries requires preparing a statement and executing it. The two-step pattern prevents SQL injection and allows parameter binding.
const stmt = db.prepare('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
stmt.run();
const insert = db.prepare('INSERT INTO users (name) VALUES (?)');
insert.run('Alice');
insert.run('Bob');
const select = db.prepare('SELECT * FROM users WHERE name = ?');
const rows = select.all('Alice');
console.log(rows); // [{ id: 1, name: 'Alice' }]The .run() method executes statements that do not return rows. The .all() method retrieves every matching row as an array. The .get() method returns the first row or undefined.
Transactions wrap multiple statements in an atomic block. Roll back if any operation fails.
const insertTx = db.prepare('INSERT INTO users (name) VALUES (?)');
db.exec('BEGIN TRANSACTION');
try {
insertTx.run('Charlie');
insertTx.run('Diana');
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}The API surface matches better-sqlite3 at the statement level. Developers familiar with .prepare(), .run(), and .all() carry that knowledge directly into node:sqlite code. The difference shows up in what the module omits, not in what it changes.
Feature Parity Check: What better-sqlite3 Had That node:sqlite Does Not
better-sqlite3 offers features that node:sqlite does not yet implement. The gap matters when advanced use cases depend on those capabilities.
flowchart LR
subgraph better-sqlite3["better-sqlite3 capabilities"]
A1("User-defined functions")
A2("Custom aggregates")
A3("Backup API")
A4("Pragma shortcuts")
A5("Column type introspection")
end
subgraph node:sqlite["node:sqlite capabilities"]
B1("Prepared statements")
B2("Transactions")
B3("Parameter binding")
B4("Row iteration")
end
A1 -.-> C("Missing in node:sqlite")
A2 -.-> C
A3 -.-> C
A4 -.-> C
A5 -.-> C
style C stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
User-defined functions let developers register JavaScript callbacks that SQLite can invoke inside queries. A better-sqlite3 codebase might register a slugify() function and call it directly in a SELECT statement. node:sqlite does not expose the sqlite3_create_function() binding.
Custom aggregates extend SQLite's built-in SUM() and AVG() with application-specific logic. Teams implementing weighted averages or geometric means rely on the aggregate API. The built-in module does not support it.
The backup API provides hot-copy functionality for live databases. Scripts that snapshot production data mid-operation use db.backup() to avoid locking the source file. node:sqlite lacks the method.
Pragma shortcuts simplify configuration. better-sqlite3 exposes db.pragma('journal_mode = WAL') as a first-class method. The built-in module requires executing raw SQL: db.exec('PRAGMA journal_mode = WAL').
Column type introspection reveals schema metadata at runtime. A migration tool might inspect existing column types before altering a table. better-sqlite3 returns type information from prepared statements. node:sqlite does not expose the underlying sqlite3_column_type() call.
The feature set aligns with the module's design goal. Node.js 24 targets the common path: CRUD operations, transactions, and schema management. Advanced hooks remain the domain of third-party libraries.
Migrating Scripts and Edge Workers: Real-World Replacement Patterns
Scripts that rely on better-sqlite3 for straightforward data persistence migrate cleanly to node:sqlite. The pattern holds when the codebase uses prepared statements, parameter binding, and transactions without touching user-defined functions or aggregates.
A typical migration replaces the import and constructor, then verifies that statement methods map directly.
// Before: better-sqlite3
import Database from 'better-sqlite3';
const db = new Database('data.db');
// After: node:sqlite
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('data.db');The statement API remains identical for .prepare(), .run(), .get(), and .all(). Code that binds parameters with ? placeholders or named bindings works unchanged.
const insert = db.prepare('INSERT INTO logs (message, level) VALUES (?, ?)');
insert.run('Application started', 'INFO');
const query = db.prepare('SELECT * FROM logs WHERE level = ?');
const rows = query.all('ERROR');Edge workers running on platforms like Cloudflare Workers or Vercel Edge Functions gain immediate compatibility. These environments reject native binaries but accept standard library modules. A worker that previously failed to deploy because better-sqlite3 required compilation now runs without modification.
flowchart LR
A("Script imports node:sqlite") --> B("Prepares statements")
B --> C("Runs queries synchronously")
C --> D("Deploys to edge worker")
D --> E("Executes without native module")
style E stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
The tradeoff surfaces when the script relies on better-sqlite3 features outside the core API. A logging tool that registers a custom format_timestamp() SQL function must rewrite that logic as JavaScript post-processing. A backup script that uses db.backup() must switch to copying the database file at the filesystem level.
Migration assessment reduces to a feature audit. Grep the codebase for .function(), .aggregate(), .backup(), and .pragma(). If those methods appear, the script requires either refactoring or staying on better-sqlite3. If the codebase only calls .prepare(), .run(), .get(), and .all(), the switch is mechanical.
Performance and Stability: Benchmarks and Production Readiness
The performance characteristics of node:sqlite match better-sqlite3 because both modules link against the same SQLite engine. Query execution time depends on the SQL itself, not the binding layer. A well-indexed SELECT runs identically in both environments.
Prepared statement overhead measures in microseconds. The difference between the two modules falls within measurement noise for typical workloads. A benchmark running 10,000 inserts in a transaction shows less than 5% variance between node:sqlite and better-sqlite3.
flowchart LR
A("Query submitted") --> B("Statement prepared")
B --> C("Parameters bound")
C --> D("SQLite engine executes")
D --> E("Results returned to JavaScript")
style D stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
The stability concern centers on module maturity. better-sqlite3 has shipped production releases since 2016. The API surface is stable. Edge cases have been identified and fixed. node:sqlite entered the standard library in Node.js 22 as experimental and stabilized in Node.js 24. The module has less real-world mileage.
Production readiness depends on risk tolerance and use case. A build script that generates static site data from a database can adopt node:sqlite immediately. The worst-case failure mode is a broken build, not lost user data. A long-running application server that handles financial transactions should wait for broader ecosystem adoption and stability reports.
The module does not introduce new failure modes beyond what SQLite itself allows. Database corruption, disk full errors, and constraint violations surface the same way in both libraries. The binding layer does not add retry logic or error recovery. Developers handle errors at the application level.
Monitoring requirements do not change. Track query execution time, transaction volume, and database file size the same way regardless of binding. The built-in module does not expose performance metrics beyond what SQLite's EXPLAIN QUERY PLAN provides.
When to Keep better-sqlite3 vs When to Switch
The decision matrix collapses to three factors: feature requirements, deployment environment, and maintenance burden.
flowchart TD
A("Evaluate codebase") --> B{"Uses user-defined functions or aggregates?"}
B -->|Yes| C("Keep better-sqlite3")
B -->|No| D{"Deploys to edge workers?"}
D -->|Yes| E("Switch to node:sqlite")
D -->|No| F{"Wants to eliminate native dependencies?"}
F -->|Yes| E
F -->|No| G("Either works — preference decision")
style C stroke:#7c9cf0,fill:#142544,color:#eaf2ff
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style G stroke:#7c9cf0,fill:#142544,color:#eaf2ff
Codebases that register JavaScript functions for use inside SQL queries must stay on better-sqlite3. The API does not have a node:sqlite equivalent. Rewriting those queries to pull data into JavaScript and apply transformations there changes the architecture and often degrades performance.
Aggregates that compute custom metrics across rows similarly require better-sqlite3. A query that calculates a percentile rank using a JavaScript aggregate function has no direct translation to the built-in module.
Deployment to edge workers flips the equation. Platforms that reject native binaries make better-sqlite3 unusable. The choice becomes node:sqlite or no SQLite at all. Scripts running in serverless functions on AWS Lambda, Google Cloud Functions, or Azure Functions gain faster cold starts by eliminating the native module. The reduction in deployment package size shortens upload time and improves function initialization.
Teams maintaining multiple projects with different Node.js versions face a version lock decision. better-sqlite3 runs on Node.js 14 and later. node:sqlite requires Node.js 24. A monorepo with services on different runtime versions cannot standardize on the built-in module until every service upgrades.
The maintenance burden calculation compares ongoing dependency updates against version lock risk. better-sqlite3 releases quarterly. Each update requires rebuilding native binaries and testing across platforms. node:sqlite updates arrive with Node.js releases. The module version stays synchronized with the runtime version. Teams already managing Node.js upgrades absorb the SQLite update as part of that process.
Libraries published to npm that depend on SQLite should continue using better-sqlite3. The library's broader Node.js compatibility ensures users on older runtimes can still install the package. A CLI tool that supports Node.js 18 cannot require Node.js 24 just to enable SQLite.
Frequently Asked Questions
Does node:sqlite support WAL mode?
Yes. Execute db.exec('PRAGMA journal_mode = WAL') after opening the database. The module does not provide a shortcut method, but the pragma works identically to better-sqlite3.
Can I use node:sqlite in production applications today?
The module is stable in Node.js 24. Production readiness depends on the application's risk profile. Build tools, scripts, and edge workers are good candidates. Mission-critical transaction systems should wait for broader ecosystem validation.
Will better-sqlite3 continue to receive updates?
The library remains actively maintained. Teams that rely on user-defined functions, custom aggregates, or older Node.js versions will continue using better-sqlite3 regardless of the built-in module's availability.
How do I migrate a database from better-sqlite3 to node:sqlite?
No migration is necessary. Both modules read and write standard SQLite database files. Copy the .db file and open it with the new module. Schema and data remain compatible.
Does node:sqlite work with TypeScript?
Yes. The module ships with type definitions in Node.js 24. Import DatabaseSync from node:sqlite and TypeScript infers statement return types automatically.
Conclusion: Native SQLite and the Future of Embedded Databases in Node.js
Node.js 24's built-in SQLite module eliminates the native dependency barrier for the majority of embedded database use cases. Scripts that execute queries, manage transactions, and handle schema changes now run without compilation steps or platform-specific binaries. Edge workers gain access to persistent storage without violating runtime constraints.
The feature gap between node:sqlite and better-sqlite3 defines the boundary. Projects that rely on user-defined functions, custom aggregates, or advanced introspection must stay on the native module. Straightforward CRUD applications, build tools, and serverless functions switch immediately.
The long-term trajectory favors the built-in module. As Node.js releases continue, the standard library API will expand based on real-world usage patterns. The ecosystem will converge on node:sqlite for new projects while maintaining better-sqlite3 for specialized requirements.
That covers the essential patterns for adopting Node.js 24's native SQLite support. Evaluate your feature requirements, audit your deployment environment, and apply the built-in module where it fits. The difference in deployment complexity will be immediate.