npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-sqlite3-pool

v1.0.1

Published

A non-blocking, multi-threaded, auto-scaling SQLite connection pool with encryption support.

Readme

better-sqlite3-pool

A non-blocking, multi-threaded, auto-scaling SQLite connection pool built on top of better-sqlite3-multiple-ciphers.

Designed for high-concurrency Node.js applications (like REST APIs or GraphQL servers) that need the speed of SQLite and the security of SQLCipher encryption without blocking the main event loop.

🚀 Why this exists?

The standard better-sqlite3 is the fastest driver available, but it is synchronous.

  1. The Blocking Problem: If a query takes 50ms, your Node.js server cannot handle any other requests during that time.
  2. The Encryption Penalty: Using SQLCipher adds heavy CPU overhead for decryption. Running this on the main thread kills throughput.

better-sqlite3-pool solves this by moving all database operations to Worker Threads.

✨ Features

  • 🚫 Non-Blocking: All queries return Promises. Your event loop stays free.
  • 🔒 Encrypted Support: Native support for SQLCipher, wxSQLite3, and AES-256.
  • ⚡ True Concurrency: Read queries run in parallel across multiple workers/cores.
  • 📈 Auto-Scaling: Spawns more Reader Workers automatically as load increases.
  • 💾 WAL-Safe Encryption: Smart handling of Journal Modes to prevent header corruption on encrypted files.
  • ❤️ Transaction Heartbeats: Auto-rollbacks stalled transactions to prevent "database locked" deadlocks.
  • 🔌 SQLite3 Adapter: Drop-in compatibility mode for legacy libraries.

📦 Installation

npm install better-sqlite3-pool

Note: You do not need to install better-sqlite3 separately.


🆚 Comparison: Original vs. Pool

| Feature | better-sqlite3 (Original) | better-sqlite3-pool (This Library) | | :--- | :--- | :--- | | API Style | Synchronous (Blocking) | Asynchronous (Promises/Await) | | Concurrency | 1 Query at a time (Serialized) | Parallel Reads (Writer + N Readers) | | Main Thread | Blocked during queries | Free to handle HTTP requests | | Encryption | Blocks event loop (High CPU) | Offloaded to Worker Threads | | Transactions | Blocks everything | Blocks Writer only (Readers continue) | | UDF Functions | Can use closures | Pure functions only (Serialized) | | Best For | Desktop Apps, CLI, Scripts | Web Servers, APIs, Electron Main |


🛠 Basic Usage

Initialization is asynchronous to ensure workers are ready.

const { Database } = require("better-sqlite3-pool");

async function main() {
  // 1. Initialize the pool (Factory Pattern)
  const db = await Database.create("my-database.db", {
    minWorkers: 2, // Always keep 2 readers alive
    maxWorkers: 4, // Scale up to 4 readers under load
  });

  // 2. Writes (Sent to the single Writer thread)
  const res = await db
    .prepare("INSERT INTO users (name) VALUES (?)")
    .run("Alice");
  
  console.log(`Inserted ID: ${res.lastInsertRowid}`);

  // 3. Reads (Load balanced across Reader threads)
  const user = await db
    .prepare("SELECT * FROM users WHERE id = ?")
    .get(res.lastInsertRowid);

  console.log(user);

  // 4. Shutdown
  await db.close();
}

main();

🔐 Encryption Usage

When using encryption, you must tell the pool the file is encrypted so it handles the WAL (Write-Ahead Log) header correctly.

Creating a new Encrypted Database file

const db = await Database.create("secure.db");

// Should rekey the database to initialize the database with key
// As key can not be set on new database opened on WAL mode
await db.pragma("rekey = 'secret-password'");

// Now you can safely enable WAL mode for performance
await db.pragma("journal_mode = WAL");

await db.exec("CREATE TABLE IF NOT EXISTS confidential (data TEXT)");

Opening an existing Encrypted Database

const db = await Database.create("secure.db");

// Broadcast the key to ALL workers (Writer + Readers)
await db.pragma("key = 'secret-password'");

// Now you can safely enable WAL mode for performance
await db.pragma("journal_mode = WAL");

await db.exec("CREATE TABLE IF NOT EXISTS confidential (data TEXT)");

Rekeying (Changing Password)

// Change password from 'old' to 'new'
await db.rekey("new-password");

⚡ Transactions

Managed Transactions (Recommended)

This wrapper automatically acquires a connection, begins the transaction, runs your logic, and commits (or rolls back on error).

const insertMany = db.transaction(async (users) => {
  const stmt = db.prepare("INSERT INTO users (name) VALUES (?)");
  for (const user of users) {
    await stmt.run(user);
  }
});

// Takes an exclusive lock on the Writer
await insertMany(["Bob", "Charlie", "Dave"]);

Manual Acquisition (Advanced)

If you need granular control, you can acquire an exclusive session on the Writer.

// Locks the writer worker. No other writes can happen until release().
const conn = await db.acquire();

try {
  await conn.exec("BEGIN");
  await conn.prepare("INSERT INTO log VALUES (?)").run("Log 1");

  // Do some heavy calculation...

  await conn.prepare("INSERT INTO log VALUES (?)").run("Log 2");
  await conn.exec("COMMIT");
} catch (err) {
  await conn.exec("ROLLBACK");
} finally {
  // CRITICAL: Must release to unlock the pool
  conn.release();
}

Safety: If your code crashes or hangs while holding a connection, the transactionTimeout (default 30s) will automatically rollback and release the lock.


🌊 Streaming (Iterators)

For large datasets, use .iterate(). This uses a backpressure mechanism to stream rows from the worker without loading them all into RAM.

const stmt = db.prepare("SELECT * FROM huge_table");

for await (const row of stmt.iterate()) {
  console.log(row.name);
  // The worker pauses fetching until you ask for the next row
}

🔌 Legacy / TypeORM Adapter

If you are using TypeORM, Sequelize, or a library expecting the legacy sqlite3 callback API, use the included adapter.

// In TypeORM options
const { Database, verbose } = require("better-sqlite3-pool/adapter");

const dataSource = new DataSource({
  type: "sqlite",
  database: "test.db",
  driver: {
    Database: Database, // Pass the adapter class
    verbose: verbose    // Optional
  },
  // ... other options
});

⚙️ Configuration Options

const db = await Database.create("file.db", {
  // Worker Pool Settings
  minWorkers: 1,        // Minimum readers (Default: 1)
  maxWorkers: 4,        // Maximum readers (Default: 2)

  // Database Settings
  readonly: false,      // Open in read-only mode
  fileMustExist: false, // Throw if file missing
  nativeBinding: null,  // Path to custom .node addon

  // Timeouts
  timeout: 5000,              // SQLite busy timeout
  transactionTimeout: 30000,  // Max time a transaction can stay idle
  connectionMaxLife: 60000,   // Max duration of an acquired connection

  // Logging
  verbose: console.log, // Log executed SQL
});

⚠️ Limitations & Gotchas

  1. User Defined Functions (UDFs):
    • Since functions run in a separate Worker thread, they cannot close over variables from your main thread. They must be "pure" or self-contained.
    • Bad: let count = 0; db.function('fn', () => count++) (Count stays 0 in main thread).
    • Good: db.function('add', (a, b) => a + b)
  2. In-Memory Databases:
    • :memory: databases cannot be shared across threads. If you use one, the pool effectively becomes a single-threaded wrapper around the Writer.
  3. Host Parameters:
    • Standard better-sqlite3 allows binding standard JS objects. Since we pass data via postMessage, arguments must be serializable (no Functions, Promises, or Symbols as parameters).

📜 License

MIT