Configure n8n-mcp Database Adapters: better-sqlite3 vs sql.js for Memory vs Performance
n8n-mcp automatically selects better-sqlite3 for production performance or falls back to sql.js for portability, with the native driver reducing memory usage from ~900 MiB to ~68 MiB through connection pooling while the JavaScript adapter loads the entire database into a Uint8Array for zero-dependency deployments.
The n8n-mcp server stores all node metadata in a SQLite database and abstracts the underlying driver behind a unified DatabaseAdapter interface. Understanding how to configure n8n-mcp database adapters—choosing between the native better-sqlite3 driver and the pure-JavaScript sql.js implementation—is critical for optimizing memory consumption and query performance across different deployment environments.
How n8n-mcp Automatically Selects Database Adapters
The adapter factory in [src/database/database-adapter.ts](https://github.com/czlonkowski/n8n-mcp/blob/main/src/database/database-adapter.ts) implements a try-catch strategy that prioritizes native performance while ensuring cross-platform compatibility. The createDatabaseAdapter function first attempts to instantiate better-sqlite3, a native module that binds directly to the operating system’s SQLite library.
If the native module fails to load—typically due to NODE_MODULE_VERSION mismatches or missing compiled binaries—the factory catches the error and transparently falls back to sql.js, a WebAssembly-based implementation that requires no native dependencies.
// src/database/database-adapter.ts
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
try {
// Try native driver
const adapter = await createBetterSQLiteAdapter(dbPath);
return adapter;
} catch (error) {
// Detect version-mismatch warnings and log them
if (errorMessage.includes('NODE_MODULE_VERSION')) { /* ... */ }
// Fall back to sql.js
const adapter = await createSQLJSAdapter(dbPath);
return adapter;
}
}
Memory vs Performance Trade-offs
The choice between adapters creates distinct operational characteristics regarding speed, memory footprint, and persistence guarantees.
| Aspect | better-sqlite3 | sql.js |
|---|---|---|
| Speed | Fastest – uses compiled C library, native I/O, zero-copy result sets | Slower – JavaScript engine parses queries; difference negligible for read-heavy workloads |
| Memory | Low – data lives on disk; only result rows resident | Higher – entire DB file loaded into Uint8Array in V8 memory |
| Persistence | Writes flushed immediately or via WAL | Changes kept in-memory; persisted only when scheduleSave() triggers |
| Portability | Requires binary compiled for exact Node version and OS | Pure-JS/wasm; works anywhere including browsers and serverless |
| FTS5 Support | Full-text search via native FTS5 extension | Not supported – checkFTS5Support() returns false |
| Typical Use | Production where performance and low memory are critical | Development, CI, or environments without native binary support |
Singleton Pattern for Memory Optimization
When the native driver is available, n8n-mcp uses a singleton SharedDatabase ([src/database/shared-database.ts](https://github.com/czlonkowski/n8n-mcp/blob/main/src/database/shared-database.ts)) that maintains a single native connection across all HTTP sessions. This architectural choice reduces per-session memory from approximately 900 MiB (one connection per session) to roughly 68 MiB for the entire process.
When falling back to SQLJSAdapter, the entire database resides in a Uint8Array buffer. To mitigate memory churn, the adapter implements a deferred persistence strategy controlled by SQLJS_SAVE_INTERVAL_MS (default 5 seconds), saving the buffer to disk only after periods of inactivity. This creates a maximum 5-second window of potential data loss acceptable for rebuild operations, which constitute the only heavy-write workload in the system.
Performance Evidence from Integration Tests
The integration test suite in [tests/integration/database/performance.test.ts](https://github.com/czlonkowski/n8n-mcp/blob/main/tests/integration/database/performance.test.ts) provides empirical benchmarks comparing both adapters:
- Bulk inserts: 5,000 nodes complete in under 2,000 ms with better-sqlite3 and approximately 3,000 ms with sql.js
- Indexed searches: Queries against 10,000-row tables execute in under 50 ms (native) versus roughly 80 ms (sql.js)
- Concurrent reads: 100 simultaneous
getNodecalls average ≤ 1 ms per read on native versus approximately 2 ms on sql.js
These results demonstrate that while better-sqlite3 delivers superior latency, sql.js remains perfectly viable for typical read-heavy n8n-mcp workloads while eliminating binary dependency management.
Deployment Scenarios and Configuration
Production Deployments (Docker/K8s, VMs, Bare Metal)
Ensure the better-sqlite3 binary matches your Node.js version by running npm rebuild better-sqlite3 or using the official pre-built Docker image. This configuration provides the lowest memory footprint, fastest query execution, and immediate persistence guarantees required for production stability.
Continuous Integration and GitHub Actions
CI environments often run Node versions different from development machines, causing native module load failures. The automatic fallback to sql.js handles this transparently. For critical test suites, reduce the persistence window by setting:
export SQLJS_SAVE_INTERVAL_MS=1000
npm test
Serverless and Edge Environments
Native binaries are typically prohibited in serverless functions. Force the JavaScript path by setting MCP_MODE=stdio (which silences native-fallback warnings) and ensuring sql.js is pre-installed in your deployment package.
Forcing a Specific Adapter
While the factory attempts automatic selection, you can implement manual override logic by checking the MCP_FORCE_ADAPTER environment variable:
// Modify createDatabaseAdapter to respect:
if (process.env.MCP_FORCE_ADAPTER === 'sql.js') {
return createSQLJSAdapter(dbPath);
}
if (process.env.MCP_FORCE_ADAPTER === 'better-sqlite3') {
return createBetterSQLiteAdapter(dbPath);
}
Implementation Examples
Creating a Shared Database Instance
The standard entry point for the MCP server uses the singleton pattern to minimize memory overhead:
// src/mcp/server.ts (excerpt)
import { getSharedDatabase } from './database/shared-database';
async function startMcp() {
const { db, repository, templateService } = await getSharedDatabase(
process.env.NODES_DB_PATH ?? './data/nodes.db'
);
// Repository uses the selected adapter (better-sqlite3 or sql.js)
const allNodes = repository.getAllNodes(100);
console.log(`Loaded ${allNodes.length} nodes`);
}
Explicitly Instantiating sql.js for Portable Scripts
For maintenance scripts where native binaries are unavailable, directly instantiate the fallback adapter:
import { createSQLJSAdapter } from './src/database/database-adapter';
import { NodeRepository } from './src/database/node-repository';
import path from 'path';
async function run() {
const adapter = await createSQLJSAdapter(path.resolve('./data/nodes.db'));
const repo = new NodeRepository(adapter);
const count = repo.getNodeCount();
console.log(`DB contains ${count} nodes`);
}
run();
Adjusting sql.js Persistence Interval
Modify the autosave frequency to balance durability against I/O overhead:
# Save every 2 seconds instead of the default 5 seconds
export SQLJS_SAVE_INTERVAL_MS=2000
npm run start
Summary
- n8n-mcp abstracts SQLite access through the
DatabaseAdapterinterface insrc/database/database-adapter.ts, automatically selecting better-sqlite3 when native binaries are compatible - The better-sqlite3 adapter minimizes memory usage through disk-based storage and enables the
SharedDatabasesingleton pattern, reducing process memory from ~900 MiB to ~68 MiB - sql.js provides zero-dependency portability by loading the entire database into a
Uint8Array, persisting to disk only every 5 seconds (configurable viaSQLJS_SAVE_INTERVAL_MS) - Performance benchmarks show better-sqlite3 executes bulk inserts ~33% faster and indexed searches ~38% faster than sql.js, though both adapters handle concurrent reads within acceptable latency thresholds
- Production environments benefit from native driver compilation, while CI and serverless deployments rely on the automatic or forced sql.js fallback
Frequently Asked Questions
How do I force n8n-mcp to use sql.js instead of better-sqlite3?
While the factory automatically falls back to sql.js when native binaries fail to load, you can force the JavaScript implementation by setting an environment variable MCP_FORCE_ADAPTER=sql.js and modifying the createDatabaseAdapter function to check this variable before attempting native instantiation. Alternatively, in serverless environments, simply pre-install sql.js and omit better-sqlite3 from your deployment bundle.
What are the memory implications of using sql.js versus better-sqlite3?
The better-sqlite3 adapter maintains data on disk and streams result sets, resulting in minimal resident memory. In contrast, sql.js loads the entire database file into a JavaScript Uint8Array, significantly increasing heap usage proportional to your database size. However, the SharedDatabase singleton pattern ensures that even with better-sqlite3, only one connection exists per process rather than one per HTTP session.
Does sql.js support full-text search (FTS5)?
No. According to the source code in src/database/database-adapter.ts, the checkFTS5Support() method returns false when using sql.js because the WebAssembly build does not include SQLite’s FTS5 extension. If your workflow requires full-text search capabilities on node metadata, you must use the better-sqlite3 adapter.
How often does sql.js persist data to disk?
By default, the SQLJSAdapter saves the in-memory database buffer to disk every 5 seconds (5000 ms) when scheduleSave() detects inactivity, controlled by the SQLJS_SAVE_INTERVAL_MS environment variable. This means a crash could result in losing up to 5 seconds of writes, which is generally acceptable for n8n-mcp’s read-heavy operational profile where rebuilds are infrequent.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →