SQLite storage adapter for Keyv with multi-driver support for
nodejs,bun, and custom drivers.
SQLite storage adapter for Keyv.
- Install
- Usage
- Using createKeyv
- Multi-Driver Support
- Creating a Custom Driver
- Using sqlite3
- Migrating to v6
- Constructor Options
- Properties
- Methods
- Clearing Expired Keys
- WAL Mode
- Benchmarks
- License
npm install --save keyv @keyv/sqliteimport Keyv from 'keyv';
import KeyvSqlite from '@keyv/sqlite';
const keyv = new Keyv({ store: new KeyvSqlite('sqlite://path/to/database.sqlite') });
keyv.on('error', err => console.error(err));You can specify the table, busyTimeout, and wal options:
const keyvSqlite = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
table: 'cache',
busyTimeout: 10000,
wal: true,
});
const keyv = new Keyv({ store: keyvSqlite });The createKeyv helper creates a Keyv instance with KeyvSqlite as the store in one call:
import { createKeyv } from '@keyv/sqlite';
// With a URI string
const keyv = createKeyv('sqlite://path/to/database.sqlite');
// With an options object
const keyv = createKeyv({
uri: 'sqlite://path/to/database.sqlite',
table: 'cache',
wal: true,
});@keyv/sqlite supports multiple SQLite drivers and automatically selects the best one available for your runtime:
| Driver | Package | Runtime | Type |
|---|---|---|---|
better-sqlite3 |
better-sqlite3 |
Node.js | Synchronous (fallback) |
node:sqlite |
Built-in | Node.js 22.5+ | Synchronous |
bun:sqlite |
Built-in | Bun | Synchronous |
better-sqlite3 is included as a direct dependency and used as a fallback when native runtime drivers are unavailable. On Bun, the native bun:sqlite driver is preferred. On Node.js 22.5+, the built-in node:sqlite driver is preferred. If you still need to use sqlite3 then go to the using sqlite3.
You can explicitly choose a driver via the driver option:
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
driver: 'better-sqlite3', // or 'node:sqlite' or 'bun:sqlite'
});When no driver is specified, the adapter tries drivers in this order:
- Bun:
bun:sqlitethenbetter-sqlite3 - Node.js:
node:sqlitethenbetter-sqlite3
You can pass a custom driver object that implements the SqliteDriver interface. A custom driver must provide a name and a connect() method that returns { query, close }:
import KeyvSqlite from '@keyv/sqlite';
import type { SqliteDriver } from '@keyv/sqlite';
const customDriver: SqliteDriver = {
name: 'custom',
async connect(options) {
// options: { filename: string, busyTimeout?: number, wal?: boolean }
return {
async query(sql, ...params) {
// Execute SQL and return rows for SELECT/PRAGMA, empty array for mutations
},
async close() {
// Close the database connection
},
};
},
};
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
driver: customDriver,
});The query function must return an array of row objects for SELECT and PRAGMA statements, and an empty array for all other statements (INSERT, UPDATE, DELETE, etc.).
The following types are available for building custom drivers:
import type {
SqliteDriver, // Driver interface: { name, connect() }
SqliteDriverName, // 'better-sqlite3' | 'node:sqlite' | 'bun:sqlite' | 'custom'
KeyvSqliteOptions, // Constructor options
Sqlite3ModuleLike, // Structural type for the sqlite3 module
Sqlite3DatabaseLike, // Structural type for a sqlite3.Database instance
} from '@keyv/sqlite';The createSqlite3Driver export is a real-world example of a custom driver — see Using sqlite3.
The callback-based sqlite3 package is not auto-detected or bundled with @keyv/sqlite. If you need to use it, install it in your project and pass it via the createSqlite3Driver helper:
npm install sqlite3import KeyvSqlite, { createSqlite3Driver } from '@keyv/sqlite';
import sqlite3 from 'sqlite3';
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
driver: createSqlite3Driver(sqlite3),
});sqlite3.verbose() also works:
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
driver: createSqlite3Driver(sqlite3.verbose()),
});All standard options (wal, busyTimeout, etc.) are supported.
The opts getter still exists for backward compatibility and returns all current settings as a plain object. New top-level getters and setters have been added for namespace and clearExpiredInterval:
store.namespace = 'my-namespace';
store.clearExpiredInterval = 60_000;In v5, namespaces were stored as key prefixes in the key column (e.g. key="myns:mykey" with no namespace column). In v6, the namespace is stored in a dedicated namespace column (e.g. key="mykey", namespace="myns"). This enables more efficient queries and proper namespace isolation.
The adapter automatically detects old schemas and migrates existing data on connect — no manual migration steps are needed. During migration, prefixed keys like myns:mykey are split into key="mykey" and namespace="myns".
The adapter now extends Hookified instead of a custom EventEmitter. Events work the same (on, emit), but hooks are also available via the standard Hookified API.
v6 adds an expires BIGINT column to the table. When values are stored with a TTL via Keyv core, the adapter automatically extracts the expires timestamp from the serialized value and stores it in the column. A partial index is created on the expires column for efficient cleanup queries.
The schema migration is automatic on connect — existing tables get the column added via ALTER TABLE ... ADD COLUMN.
A new utility method that deletes all rows where the expires column is set and the timestamp is in the past:
await store.clearExpired();Set an interval (in milliseconds) to automatically call clearExpired() on a schedule. Disabled by default (0). The timer uses unref() so it won't keep the Node.js process alive.
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
clearExpiredInterval: 60_000, // clean up every 60 seconds
});New methods for efficient multi-key operations:
.setMany(entries)— bulk upsert with automatic batching (249 entries per batch to stay within SQLite's 999 parameter limit).getMany(keys)— bulk retrieve with automatic batching.deleteMany(keys)— bulk delete with automatic batching.hasMany(keys)— bulk existence check
A convenience function to create a Keyv instance with KeyvSqlite as the store in one call:
import { createKeyv } from '@keyv/sqlite';
const keyv = createKeyv('sqlite://path/to/database.sqlite');v6 replaces the callback-based sqlite3 package with better-sqlite3 as the default driver and adds support for node:sqlite (Node.js 22.5+) and bun:sqlite (Bun). The driver is auto-detected or can be explicitly selected via the driver option. See Multi-Driver Support for details.
The iterator now uses cursor-based (keyset) pagination instead of OFFSET. This handles concurrent modifications during iteration without skipping entries and is more efficient for large datasets.
KeyvSqlite accepts a connection URI string or an options object:
| Option | Type | Default | Description |
|---|---|---|---|
uri |
string |
'sqlite://:memory:' |
SQLite connection URI |
table |
string |
'keyv' |
Table name for key-value storage |
keySize |
number |
255 |
Maximum key column length (VARCHAR length, max 65535). Alias: keyLength |
namespaceLength |
number |
255 |
Maximum namespace column length (VARCHAR length) |
busyTimeout |
number |
undefined |
SQLite busy timeout in milliseconds |
iterationLimit |
number |
10 |
Number of rows fetched per batch during iteration |
wal |
boolean |
false |
Enable WAL mode for better concurrency |
clearExpiredInterval |
number |
0 |
Interval in milliseconds to automatically clear expired entries (0 = disabled) |
driver |
string | SqliteDriver |
undefined |
Explicit driver selection ('better-sqlite3', 'node:sqlite', 'bun:sqlite') or custom driver object. Auto-detected if omitted |
Get or set the namespace for the adapter. Used for key prefixing and scoping operations like clear() and iterator().
- Type:
string | undefined - Default:
undefined
const store = new KeyvSqlite('sqlite://path/to/database.sqlite');
store.namespace = 'my-namespace';
console.log(store.namespace); // 'my-namespace'Get the SQLite connection URI.
- Type:
string - Default:
'sqlite://:memory:'
const store = new KeyvSqlite('sqlite://path/to/database.sqlite');
console.log(store.uri); // 'sqlite://path/to/database.sqlite'Get or set the table name used for storage. The name is sanitized and escaped for safe use in SQL queries to prevent SQL injection.
- Type:
string - Default:
'keyv'
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', table: 'cache' });
console.log(store.table); // 'cache'
store.table = 'sessions';Get or set the maximum key length (VARCHAR length) for the key column.
- Type:
number - Default:
255
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', keySize: 512 });
console.log(store.keySize); // 512Get or set the maximum namespace column length (VARCHAR length).
- Type:
number - Default:
255
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', namespaceLength: 128 });
console.log(store.namespaceLength); // 128Get the resolved file path for the SQLite database, derived from the URI.
- Type:
string - Default:
':memory:'
const store = new KeyvSqlite('sqlite://data/app.sqlite');
console.log(store.db); // 'data/app.sqlite'Get or set the number of rows to fetch per iteration batch.
- Type:
number - Default:
10
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', iterationLimit: 50 });
console.log(store.iterationLimit); // 50Get whether WAL (Write-Ahead Logging) mode is enabled.
- Type:
boolean - Default:
false
const store = new KeyvSqlite({ uri: 'sqlite://path/to/database.sqlite', wal: true });
console.log(store.wal); // trueGet the SQLite busy timeout in milliseconds.
- Type:
number | undefined - Default:
undefined
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', busyTimeout: 5000 });
console.log(store.busyTimeout); // 5000Get the explicit driver selection. Returns undefined when auto-detected.
- Type:
string | SqliteDriver | undefined - Default:
undefined(auto-detected)
const store = new KeyvSqlite({ uri: 'sqlite://:memory:', driver: 'better-sqlite3' });
console.log(store.driver); // 'better-sqlite3'Get the name of the resolved driver after connection. This is useful to check which driver was auto-detected.
- Type:
string | undefined - Default:
undefined(set after connection is established)
const store = new KeyvSqlite('sqlite://:memory:');
await store.ready;
console.log(store.driverName); // 'better-sqlite3', 'node:sqlite', 'bun:sqlite', or 'custom'Get or set the interval in milliseconds between automatic expired-entry cleanup runs. When set to a value greater than 0, the adapter will automatically call clearExpired() at the specified interval. The timer uses unref() so it won't keep the Node.js process alive. Setting to 0 disables the automatic cleanup.
- Type:
number - Default:
0(disabled)
// Clean up expired entries every 60 seconds
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
clearExpiredInterval: 60_000,
});
console.log(store.clearExpiredInterval); // 60000
// Disable it later
store.clearExpiredInterval = 0;A promise that resolves when the database connection and schema setup are complete. You can optionally await this before the first operation to ensure the adapter is fully initialized.
- Type:
Promise<void>
const store = new KeyvSqlite('sqlite://path/to/database.sqlite');
await store.ready; // connection and schema migration completeGet all current settings as a plain object. This getter exists for backward compatibility.
- Type:
KeyvSqliteOptions
const store = new KeyvSqlite({
uri: 'sqlite://:memory:',
table: 'cache',
wal: true,
});
console.log(store.uri); // 'sqlite://:memory:'
console.log(store.table); // 'cache'Set a key-value pair. Returns true on success, false on failure.
key(string) - The key to set.value(any) - The value to store.ttl(number, optional) - Time to live in milliseconds.- Returns:
Promise<boolean>
await keyv.set('foo', 'bar');
await keyv.set('foo', 'bar', 5000); // expires in 5 secondsSet multiple key-value pairs at once. Each entry is a KeyvEntry<Value> object ({ key: string, value: Value, ttl?: number }), where Value is inferred from the entries provided. Entries are automatically batched (249 per batch) to stay within SQLite's bind parameter limit. Returns a boolean[] with per-entry success tracking. Each batch is atomic — if a batch fails, entries in that batch return false while entries in successful batches return true. On batch failure, an error event is emitted.
const results = await keyv.setMany([
{ key: 'foo', value: 'bar' },
{ key: 'baz', value: 'qux' },
]); // [true, true]Get a value by key. Returns undefined if the key does not exist.
const value = await keyv.get('foo'); // 'bar'Get multiple values at once. Returns an array of values in the same order as the keys, with undefined for missing keys.
const values = await keyv.getMany(['foo', 'baz']); // ['bar', 'qux']Check if a key exists. Returns a boolean.
const exists = await keyv.has('foo'); // trueCheck if multiple keys exist. Returns an array of booleans in the same order as the input keys.
const results = await keyv.hasMany(['foo', 'baz', 'unknown']); // [true, true, false]Delete a key. Returns true if the key existed, false otherwise.
const deleted = await keyv.delete('foo'); // trueDelete multiple keys at once. Returns a boolean[] indicating whether each key existed.
const results = await keyv.deleteMany(['foo', 'baz']); // [true, true]Clear all keys in the current namespace.
await keyv.clear();Utility helper method to delete all expired entries from the store. This removes any rows where the expires column is set and the timestamp is in the past. This is useful for periodic cleanup of expired data.
await store.clearExpired();Iterate over all key-value pairs. The iterator uses the namespace configured on the instance. Uses cursor-based pagination controlled by the iterationLimit option.
const iterator = keyv.iterator();
for await (const [key, value] of iterator) {
console.log(key, value);
}Disconnect from the SQLite database and release resources. Stops the automatic expired-entry cleanup interval if running.
await store.disconnect();When a key is stored with a TTL, the adapter records the expiration timestamp in the expires column. Keyv core enforces TTL automatically — expired keys return undefined from get() and false from has(), and are lazily deleted from the store when accessed via get(), getMany(), or iteration.
However, expired rows that are never accessed again will remain in the database. The clearExpired() method and clearExpiredInterval option provide bulk cleanup to remove these stale rows efficiently via SQL, without needing to deserialize every row.
Set the clearExpiredInterval option (in milliseconds) to automatically remove expired entries on a recurring timer. The timer uses unref() so it won't keep the Node.js process alive.
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
clearExpiredInterval: 60_000, // clean up every 60 seconds
});You can change or disable the interval at runtime:
// Change to every 5 minutes
store.clearExpiredInterval = 300_000;
// Disable automatic cleanup
store.clearExpiredInterval = 0;Call clearExpired() directly to remove all expired entries on demand:
await store.clearExpired();By default, SQLite uses the rollback journal for transactions. Enabling WAL (Write-Ahead Logging) mode can significantly improve concurrency and write performance for most workloads.
const store = new KeyvSqlite({
uri: 'sqlite://path/to/database.sqlite',
wal: true,
});
const keyv = new Keyv({ store });Note: WAL mode is not supported for in-memory databases (:memory:). If enabled for an in-memory database, a warning will be logged and the option will be ignored.
From the SQLite documentation:
WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently. WAL is significantly faster than the default rollback journal in most scenarios involving a single database connection, and is also faster in many scenarios involving multiple database connections.
Simple set / get benchmarks comparing the built-in SQLite drivers plus an optional sqlite3 custom-driver setup using in-memory databases with 10,000 pre-generated key-value pairs. Results will vary across machines and runs — they are meant as a relative comparison, not absolute performance numbers.
| name | summary | ops/sec | time/op | margin | samples |
|---|---|---|---|---|---|
| bun set / get | 🥇 | 64K | 18µs | ±0.79% | 57K |
| better set / get | -32.0% | 44K | 25µs | ±2.34% | 40K |
| node set / get | -32.7% | 43K | 25µs | ±2.46% | 40K |
| sqlite3 set / get | -74.7% | 16K | 67µs | ±1.25% | 15K |
Note: we included sqlite3 tests in this but by default we do not have it as a dependency as our fallback is better-sqlite3 now. Please refor to using sqlite3 if you want to use it.