# Configure n8n-mcp Database Adapters: better-sqlite3 vs sql.js for Memory vs Performance

> Configure n8n-mcp database adapters for optimal performance or portability. Compare better-sqlite3 for memory efficiency vs sql.js for zero-dependency deployments.

- Repository: [Romuald Członkowski/n8n-mcp](https://github.com/czlonkowski/n8n-mcp)
- Tags: performance
- Published: 2026-03-24

---

**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)](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.

```typescript
// 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)](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)](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 `getNode` calls 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:

```bash
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:

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```bash

# 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 `DatabaseAdapter` interface in [`src/database/database-adapter.ts`](https://github.com/czlonkowski/n8n-mcp/blob/main/src/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 `SharedDatabase` singleton 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 via `SQLJS_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`](https://github.com/czlonkowski/n8n-mcp/blob/main/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.