Fix: ElizaOS SQLITE_BUSY: Database is Locked in Parallel Agent Swarms
When scaling autonomous agent deployments using ElizaOS to run multi-agent swarms (e.g. running 3 to 10 agent personas within a single Node.js process or across shared Docker container mounts), developers frequently encounter intermittent runtime crashes:
SqliteError: database is locked
at Database.prepare (/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
at SqliteDatabaseAdapter.createMemory (/packages/adapter-sqlite/src/index.ts:184:10)
at AgentRuntime.messageManager.createMemory (/packages/core/src/runtime.ts:512:22)
[FATAL] Unhandled rejection: SQLITE_BUSY (error code 5)
The error appears during high-traffic message spikes, coordinated group chats, or automated DEX trading loops when multiple agents attempt to persist memories, evaluate sentiment, or record transaction hashes simultaneously.
If your multi-agent cluster is also experiencing container restarts due to missing environment configurations, consult our guide on Fixing ElizaOS Docker-Compose Environment Variables Crashes.
Architectural Deep-Dive: SQLite Concurrency Model and Lock Contention
By default, @elizaos/adapter-sqlite initializes better-sqlite3 using SQLite’s traditional Delete / Rollback Journal mode.
The Core Bottleneck: Exclusive Locking
In standard rollback journal mode:
- Exclusive Write Lock: Any agent writing to the database obtains an exclusive file lock (
EXCLUSIVE_LOCK) ondb.sqlite. - Zero Reader Tolerance: While an agent holds an exclusive write lock, all other agents attempting to read context memories or write responses are rejected.
- Default Zero Busy Timeout: If
better-sqlite3encounters a lock without an explicitbusy_timeoutconfiguration, it immediately throwsSQLITE_BUSYrather than sleeping for a few milliseconds to wait for lock release.
Step-by-Step Resolution Protocol
To eliminate SQLITE_BUSY errors without requiring code refactors across all your agent character files, apply this three-tier optimization protocol.
1. Enable Write-Ahead Logging (WAL) and Busy Timeout via PRAGMA
In your ElizaOS project root or inside packages/adapter-sqlite/src/index.ts, enforce WAL mode and configure a 5,000 ms busy timeout when initializing the SQLite instance:
// packages/adapter-sqlite/src/index.ts (Optimized initialization)
import Database from 'better-sqlite3';
export function createOptimizedSqliteDatabase(dbPath: string): Database.Database {
const db = new Database(dbPath, {
timeout: 5000, // Wait up to 5 seconds before throwing SQLITE_BUSY
verbose: process.env.DEBUG_SQLITE === 'true' ? console.log : undefined,
});
// CRITICAL OPTIMIZATION 1: Enable Write-Ahead Logging (WAL)
// Readers and writers no longer block one another
db.pragma('journal_mode = WAL');
// CRITICAL OPTIMIZATION 2: Set busy timeout at the engine level
db.pragma('busy_timeout = 5000');
// CRITICAL OPTIMIZATION 3: Relax synchronous fsync without risking corruption
db.pragma('synchronous = NORMAL');
// CRITICAL OPTIMIZATION 4: Increase in-memory cache size to 64MB
db.pragma('cache_size = -64000');
// CRITICAL OPTIMIZATION 5: Store temporary tables in memory
db.pragma('temp_store = MEMORY');
return db;
}
2. Implement an Exponential Backoff Retry Wrapper
Wrap critical database write operations (such as createMemory and setGoal) in an exponential backoff decorator that catches transient SQLITE_BUSY errors and retries automatically:
// src/utils/sqliteRetry.ts
export async function withSqliteRetry<T>(
operation: () => Promise<T> | T,
maxRetries = 5,
baseDelayMs = 50
): Promise<T> {
let attempt = 0;
while (true) {
try {
return await operation();
} catch (error: any) {
attempt++;
const isBusy = error?.code === 'SQLITE_BUSY' || error?.message?.includes('database is locked');
if (!isBusy || attempt >= maxRetries) {
throw error;
}
// Exponential jitter backoff (e.g., 50ms, 100ms, 200ms...)
const delay = baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 25;
console.warn(`[SqliteRetry] Database locked. Retrying attempt ${attempt}/${maxRetries} in ${Math.round(delay)}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
3. Apply WAL Settings Directly to Existing db.sqlite
If your agents are currently running in production and you cannot immediately recompile packages, apply the WAL mode directly to your database file using the SQLite3 command-line CLI:
# Verify current journal mode (returns 'delete')
sqlite3 data/db.sqlite "PRAGMA journal_mode;"
# Activate WAL mode (returns 'wal')
sqlite3 data/db.sqlite "PRAGMA journal_mode = WAL;"
# Set persistent busy timeout
sqlite3 data/db.sqlite "PRAGMA busy_timeout = 5000;"
# Check generated companion files (db.sqlite-wal and db.sqlite-shm should now appear)
ls -la data/
Note: Once WAL mode is active, SQLite will create two temporary companion files: db.sqlite-wal (write log) and db.sqlite-shm (shared memory index). Ensure your Docker container mounts include these files!
Performance & Concurrency Comparison Table
| Metric / Setting | Default Rollback Mode | WAL Mode + Busy Timeout | PostgreSQL Migration |
|---|---|---|---|
| Concurrency Model | Single Writer OR Single Reader | Single Writer AND Multi-Reader | Multi-Writer AND Multi-Reader (MVCC) |
| Lock Timeout | 0 ms (Fails immediately) | 5,000 ms (Queues and waits) | Configurable (statement_timeout) |
| Max Parallel Agents | 1–2 agents | 5–15 agents | 100+ agents |
| Write Performance | ~120 writes/sec | ~1,800 writes/sec | High (Clustered) |
| File Footprint | Single .sqlite file | .sqlite + -wal + -shm | Dedicated database server |
Frequently Asked Questions
Q: Does WAL mode increase the risk of database corruption if the server suddenly loses power?
No. In conjunction with PRAGMA synchronous = NORMAL;, WAL mode provides robust crash resilience. In the event of a sudden power outage, any uncommitted transactions in the -wal file are simply discarded during automatic recovery on the next startup.
Q: Why do I see db.sqlite-wal growing to hundreds of megabytes?
If long-running read queries remain open indefinitely, SQLite cannot checkpoint the WAL file back into the primary database. Ensure your agent connections close idle cursors, or periodically execute PRAGMA wal_checkpoint(TRUNCATE); via a cron task.
Q: Can multiple Docker containers share a WAL database over NFS or SMB network drives?
NEVER use SQLite WAL mode over networked filesystems (NFS, SMB, CIFS). SQLite relies on POSIX advisory locks and shared memory (-shm) primitives that are broken or inconsistent across network file protocols. If your agents run across multiple servers, migrate to PostgreSQL.