better-sqlite3, sqlite, and sqlite3 are all Node.js bindings for the SQLite database engine, allowing developers to store and query data locally without a separate database server. sqlite3 is the legacy, widely-used asynchronous binding based on callbacks and promises. better-sqlite3 is a modern, synchronous binding known for high performance and a simpler API, though it blocks the event loop during execution. sqlite (often associated with sql.js or pure JS implementations) typically refers to a WebAssembly-based or fully asynchronous wrapper designed for portability across environments like browsers and Node.js, prioritizing compatibility over raw speed.
When adding a local database to a Node.js project, SQLite is the go-to choice for its zero-configuration setup and single-file storage. However, the JavaScript binding you choose dictates your application's architecture, performance ceiling, and deployment targets. The three main contenders—better-sqlite3, sqlite (often sql.js), and sqlite3—take fundamentally different approaches to bridging C++ and JavaScript. Let's break down how they work and when to use each.
The most critical difference lies in how these libraries handle the Node.js event loop.
better-sqlite3 runs synchronously. Every query blocks the main thread until the database returns a result. While this sounds scary, it actually makes code easier to read and debug because you don't need await or callbacks for every step. It is incredibly fast for heavy workloads.
// better-sqlite3: Synchronous execution
const Database = require('better-sqlite3');
const db = new Database('my-db.sqlite');
// No await needed; runs immediately
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
console.log(row);
// Transactions are simple functions
const insert = db.prepare('INSERT INTO users (name) VALUES (?)');
const insertMany = db.transaction((names) => {
for (const name of names) insert.run(name);
});
insertMany(['Alice', 'Bob']);
sqlite3 is fully asynchronous. It uses callbacks or promises to ensure the event loop never blocks. This is great for servers handling thousands of concurrent connections where a slow query shouldn't freeze other requests, but it leads to more complex code structures.
// sqlite3: Asynchronous execution with callbacks
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('my-db.sqlite');
db.get('SELECT * FROM users WHERE id = ?', [1], (err, row) => {
if (err) throw err;
console.log(row);
});
// Closing requires checking when all queries finish
db.close((err) => {
if (err) console.error(err.message);
});
sqlite (typically referring to sql.js or similar WASM-based ports) compiles SQLite to WebAssembly. It can run synchronously or asynchronously depending on the wrapper, but its superpower is portability. It runs anywhere JavaScript runs, including browsers.
// sqlite (sql.js): Portable, often async initialization
const initSqlJs = require('sql.js');
(async () => {
const SQL = await initSqlJs();
const db = new SQL.Database();
// Run queries in memory or load from file buffer
db.run('CREATE TABLE users (id INT, name TEXT)');
db.run('INSERT INTO users VALUES (?, ?)', [1, 'Alice']);
const stmt = db.prepare('SELECT * FROM users WHERE id = :id');
stmt.bind({ ':id': 1 });
while (stmt.step()) {
console.log(stmt.getAsObject());
}
})();
How you install these packages affects your CI/CD pipeline and deployment strategy.
better-sqlite3 requires native compilation. It downloads pre-built binaries for most systems, but if your server OS isn't standard (like some ARM-based Lambdas or Alpine Linux), you might need Python and a C++ compiler installed to build it from source. This can slow down Docker builds if not cached correctly.
# May require build tools on some systems
npm install better-sqlite3
sqlite3 also relies on native binaries and has historically been notorious for installation headaches, especially when switching Node versions or OS architectures. It often forces a rebuild of the native module, which can fail in minimal container environments without extra setup.
# Frequently requires node-gyp rebuilds
npm install sqlite3
sqlite (sql.js) has zero native dependencies. It is pure JavaScript and WebAssembly. You can install it on any machine, cross-compile easily, and it works instantly in browser bundles without special webpack configuration for native modules.
# Pure JS/WASM, no build tools needed
npm install sql.js
The developer experience varies significantly between synchronous and asynchronous styles.
better-sqlite3 shines with its prepared statement API. You prepare a statement once and reuse it efficiently. Errors are thrown as standard JavaScript exceptions, making them easy to catch with try...catch blocks.
// better-sqlite3: Clean error handling
try {
const stmt = db.prepare('INSERT INTO logs (msg) VALUES (?)');
stmt.run('System started');
} catch (err) {
console.error('Database write failed:', err.message);
}
sqlite3 forces you into callback hell or promise chains unless you wrap it yourself. Error handling happens inside the callback, which can lead to swallowed errors if you forget to check the err argument.
// sqlite3: Error handling inside callbacks
db.run('INSERT INTO logs (msg) VALUES (?)', ['System started'], function(err) {
if (err) {
return console.error('Database write failed:', err.message);
}
console.log('Row inserted with ID:', this.lastID);
});
sqlite offers a C-like API adapted for JS. While powerful, it often requires manual binding of parameters and stepping through results, which feels more verbose than the high-level helpers in better-sqlite3.
// sqlite (sql.js): Manual stepping
const stmt = db.prepare('SELECT * FROM logs');
while (stmt.step()) {
const row = stmt.getAsObject();
// Process row
}
stmt.free(); // Must manually free memory
Where your code runs is a hard constraint.
better-sqlite3 is the performance king. sqlite3 is the legacy alternative.sqlite (sql.js) works here. Native bindings cannot run in Chrome or Firefox.sqlite is safest. better-sqlite3 works on some platforms (like standard AWS Lambda) but fails on others that restrict synchronous I/O or native addons.| Feature | better-sqlite3 | sqlite3 | sqlite (sql.js) |
|---|---|---|---|
| Execution | Synchronous (Blocking) | Asynchronous (Non-blocking) | Sync/Async (WASM) |
| Performance | 🚀 Fastest (Native) | ⚡ Moderate | 🐢 Slower (WASM overhead) |
| Browser Support | ❌ No | ❌ No | ✅ Yes |
| Setup Complexity | Medium (Native binaries) | High (Binary issues common) | Low (Pure JS) |
| API Style | Modern, Prepared Statements | Callback/Promise based | C-style, Manual stepping |
| Best For | CLI tools, Electron, Backend APIs | Legacy maintenance | Browser apps, Portability |
better-sqlite3 is the modern standard for Node.js development. If you are building a backend service, a desktop app with Electron, or a command-line tool, this is the package to pick. Its synchronous API removes the complexity of async database calls, and its speed is unmatched. Just ensure your deployment environment supports native modules.
sqlite (sql.js) is the universal soldier. Choose it if you need to share database logic between your Node backend and a React frontend, or if you are deploying to restrictive environments like Cloudflare Workers or browser-based IDEs. You trade some speed for the ability to run anywhere.
sqlite3 is the legacy option. While still widely installed, it offers no real advantage over better-sqlite3 for new projects. Its asynchronous nature adds code complexity without providing significant concurrency benefits for typical SQLite workloads, and its installation process is often more fragile. Reserve it for maintaining older applications that haven't been migrated yet.
Final Thought: For 90% of Node.js projects today, better-sqlite3 provides the best balance of speed, simplicity, and reliability. Reach for sqlite only when you strictly need browser compatibility or zero-native-dependency deployment.
Choose better-sqlite3 if you are building a high-performance Node.js backend, CLI tool, or desktop app (Electron) where blocking the event loop for milliseconds is acceptable. It is the best choice for complex queries, bulk operations, and transactional integrity due to its synchronous nature, which simplifies error handling and control flow. Avoid it if you need to run your code in a browser or a serverless environment that strictly forbids synchronous file I/O.
Choose sqlite (or sql.js) if you need your database logic to run in multiple environments, including web browsers, React Native, or serverless edge functions where native binaries are not allowed. It is ideal for prototypes, small-scale data caching, or applications where portability is more critical than raw query speed. Be aware that performance will be lower than native bindings, and it may lack some advanced SQLite features depending on the specific implementation.
Choose sqlite3 only if you are maintaining a legacy codebase that already depends on it or if you require a non-blocking, asynchronous API for very specific I/O-bound workflows in older Node.js versions. For new projects, it is generally recommended to avoid sqlite3 in favor of better-sqlite3 due to the latter's superior performance, cleaner API, and active maintenance, as sqlite3 can be prone to installation issues with native binaries and has a more complex asynchronous control flow.
The fastest and simplest library for SQLite in Node.js.
better-sqlite3 is used by thousands of developers and engineers on a daily basis. Long nights and weekends were spent keeping this project strong and dependable, with no ask for compensation or funding, until now. If your company uses better-sqlite3, ask your manager to consider supporting the project:
select 1 row get() | select 100 rows all() | select 100 rows iterate() 1-by-1 | insert 1 row run() | insert 100 rows in a transaction | |
|---|---|---|---|---|---|
| better-sqlite3 | 1x | 1x | 1x | 1x | 1x |
| sqlite and sqlite3 | 11.7x slower | 2.9x slower | 24.4x slower | 2.8x slower | 15.6x slower |
You can verify these results by running the benchmark yourself.
npm install better-sqlite3
Requires a currently supported Node.js version. Prebuilt binaries are available for major platforms/architectures. If you have trouble installing, check the troubleshooting guide.
const db = require('better-sqlite3')('foobar.db', options);
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
console.log(row.firstName, row.lastName, row.email);
Though not required, it is generally important to set the WAL pragma for performance reasons.
db.pragma('journal_mode = WAL');
import Database from 'better-sqlite3';
const db = new Database('foobar.db', options);
db.pragma('journal_mode = WAL');
node-sqlite3 uses asynchronous APIs for tasks that are either CPU-bound or serialized. That's not only bad design, but it wastes tons of resources. It also causes mutex thrashing which has devastating effects on performance.node-sqlite3 exposes low-level (C language) memory management functions. better-sqlite3 does it the JavaScript way, allowing the garbage collector to worry about memory management.better-sqlite3 is simpler to use, and it provides nice utilities for some operations that are very difficult or impossible in node-sqlite3.better-sqlite3 is much faster than node-sqlite3 in most cases, and just as fast in all other cases.In most cases, if you're attempting something that cannot be reasonably accomplished with better-sqlite3, it probably cannot be reasonably accomplished with SQLite in general. For example, if you're executing queries that take one second to complete, and you expect to have many concurrent users executing those queries, no amount of asynchronicity will save you from SQLite's serialized nature. Fortunately, SQLite is very very fast. With proper indexing, we've been able to achieve upward of 2000 queries per second with 5-way-joins in a 60 GB database, where each query was handling 5–50 kilobytes of real data.
If you have a performance problem, the most likely causes are inefficient queries, improper indexing, or a lack of WAL mode—not better-sqlite3 itself. However, there are some cases where better-sqlite3 could be inappropriate:
For these situations, you should probably use a full-fledged RDBMS such as PostgreSQL.
Upgrading your better-sqlite3 dependency can potentially introduce breaking changes, either in the better-sqlite3 API (if you upgrade to a new major version), or between your existing database(s) and the underlying version of SQLite. Before upgrading, review: