Fixing Claude Code's Concurrent Session Problem: Implementing Memory MCP with SQLite WAL Mode
Source: Dev.to
The Problem
Have you ever seen this error while running multiple Claude Code sessions?
Error: database is locked
Yeah, that was my first reaction too: “Wait, seriously?”
Memory MCP is an incredibly powerful tool for sharing knowledge across Claude Code sessions, but the official implementation doesn’t support concurrent access. So I built a version using SQLite’s Write‑Ahead Logging (WAL) mode to solve this problem once and for all.
Who’s this for?
- Developers using Claude Code in their daily workflow
- Anyone interested in the Model Context Protocol (MCP)
- Teams wanting to run multiple AI sessions in parallel
- Developers looking for practical SQLite implementation patterns
Why the official Memory MCP falls short
The official Memory MCP (@modelcontextprotocol/server-memory) stores knowledge graphs in JSONL files. This approach has some serious limitations:
// Simplified view of the official implementation
const data = await fs.readFile('memory.jsonl', 'utf-8');
// ... process data ...
await fs.writeFile('memory.jsonl', newData);
- No file‑locking – concurrent writes from multiple sessions can corrupt your data.
- Linear searches – the implementation reads all data into memory; as the graph grows, queries become slower.
- No atomic updates – a partial failure leaves the file in an inconsistent state, which is a deal‑breaker for production use.
My solution: SQLite‑based Memory MCP
SQLite’s WAL mode is genuinely game‑changing:
| Feature | What WAL gives you |
|---|---|
| Concurrent reads/writes | Multiple readers + one writer can access the DB simultaneously |
| Crash recovery | Data integrity is guaranteed even if the process crashes mid‑write |
| Faster writes | Log‑based approach outperforms traditional rollback journals |
It’s one of those “simple” features that changes everything.
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
export class KnowledgeGraphStore {
private db: Database.Database;
constructor(dbPath: string) {
// Create directory if it doesn’t exist
const dir = path.dirname(dbPath);
if (dir && dir !== '.') {
fs.mkdirSync(dir, { recursive: true });
}
this.db = new Database(dbPath);
// Enable WAL mode for concurrent access
this.db.pragma('journal_mode = WAL');
// Set busy timeout to wait 5 seconds on lock contention
this.db.pragma('busy_timeout = 5000');
this.initSchema();
}
}
Key points
journal_mode = WAL– enables concurrent read/write access.busy_timeout = 5000– waits 5 seconds before throwing a lock error.
Without the busy timeout you’ll get immediate lock errors. Trust me, set this.
Schema design
I structured the knowledge graph using three tables for efficient management:
private initSchema(): void {
this.db.exec(`
-- Entities (concepts) management
CREATE TABLE IF NOT EXISTS entities (
name TEXT PRIMARY KEY,
entity_type TEXT NOT NULL
);
-- Observations and facts about entities
CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_name TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (entity_name) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(entity_name, content) -- Prevent duplicates
);
-- Relationships between entities
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entity TEXT NOT NULL,
to_entity TEXT NOT NULL,
relation_type TEXT NOT NULL,
FOREIGN KEY (from_entity) REFERENCES entities(name) ON DELETE CASCADE,
FOREIGN KEY (to_entity) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(from_entity, to_entity, relation_type) -- Prevent duplicates
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_name);
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
`);
}
Design principles
| Principle | Why it matters |
|---|---|
| CASCADE | Automatically delete related data when an entity is removed (no orphaned records). |
| UNIQUE constraints | Prevent duplicate data. |
| Indexes | Dramatically improve query performance. Those indexes are crucial – without them queries slow down fast. |
Inserting data – transactions matter
createEntities(entities: Entity[]): Entity[] {
const insertEntity = this.db.prepare(
'INSERT OR IGNORE INTO entities (name, entity_type) VALUES (?, ?)'
);
const insertObservation = this.db.prepare(
'INSERT OR IGNORE INTO observations (entity_name, content) VALUES (?, ?)'
);
const created: Entity[] = [];
// Process everything in a transaction
const transaction = this.db.transaction((entities: Entity[]) => {
for (const entity of entities) {
insertEntity.run(entity.name, entity.entityType);
for (const obs of entity.observations) {
insertObservation.run(entity.name, obs);
}
created.push(entity);
}
});
transaction(entities);
return created;
}
Why transactions?
- Atomicity – failed operations don’t leave partial changes.
- Consistency – multi‑table updates maintain referential integrity.
- Performance – batch commits are significantly faster.
Without transactions you can end up with corrupted data on errors – I learned this the hard way.
Getting started
I published this as an npm package so anyone can use it easily:
npm install @pepk/mcp-memory-sqlite
Add the server to your ~/.claude.json (or global config):
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["@pepk/mcp-memory-sqlite"],
"env": {
"MEMORY_DB_PATH": "./.claude/memory.db"
}
}
}
}
Configuration tips
| Variable | Recommendation |
|---|---|
MEMORY_DB_PATH | Per‑project databases like ./.claude/memory.db are easier to manage. |
| Global sharing | Set to ~/memory.db (or similar) if you need a single DB for all projects. |
Feature comparison
| Feature | Official (JSONL) | This Implementation (SQLite + WAL) |
|---|---|---|
| Concurrent access | ❌ Not supported | ✅ Supported (WAL) |
| Transactions | ❌ None | ✅ ACID guarantees |
| Search speed | Slow (linear) | Fast (indexed) |
Data Integrity
| Level | Description |
|---|---|
| Weak | – |
| Strong | (foreign keys) |
Crash Recovery
| Feature | Status |
|---|---|
| Manual repair | ❌ |
| Automatic (WAL) | ✅ |
The differences are pretty significant.
“database is locked”
If you still get this error with WAL mode enabled, try the following:
// Increase the busy timeout
this.db.pragma('busy_timeout = 10000'); // 10 seconds
If 5 seconds isn’t enough, try 10 seconds.
Run a checkpoint to truncate the WAL file
sqlite3 memory.db "PRAGMA wal_checkpoint(TRUNCATE);"
Running this periodically keeps the WAL file size under control.
Memory MCP Implementation (SQLite WAL)
I built a Memory MCP implementation using SQLite’s WAL mode to solve concurrent‑session problems.
Key Takeaways
- WAL mode enables concurrent read/write access.
- Transactions guarantee data consistency.
- Indexes provide fast search performance.
The WAL mode’s concurrent‑access support is the real game‑changer here. Now you can safely run multiple sessions without worrying about data corruption.
npm Package
@pepk/mcp-memory-sqlite
Repository
Documentation (Japanese)
- Qiita: 日本語で読む
- Zenn: Zennで読む
- note (Story): 開発ストーリー
About the Author
Daichi Kudo
- CEO at Cognisant LLC – Building the future where humans and AI create together
- CTO at M16 LLC – AI, Creative & Engineering
If this helps solve your Claude Code concurrent‑session problems, I’d love to hear about it! Issues and PRs are always welcome.