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 This Is For
- Developers using Claude Code in their daily workflow
- Anyone interested in Model Context Protocol (MCP)
- Teams wanting to run multiple AI sessions in parallel
- Developers looking for practical SQLite implementation patterns
The Official Memory MCP's Limitations
The official Memory MCP (@modelcontextprotocol/server-memory) stores knowledge graphs in JSONL files. This approach has some serious limitations:
1. No Concurrent Access Support
// Simplified view of the official implementation
const data = await fs.readFile('memory.jsonl', 'utf-8');
// ... process data ...
await fs.writeFile('memory.jsonl', newData);
Without file locking mechanisms, concurrent writes from multiple sessions can corrupt your data. Not ideal.
2. Performance Degradation
The implementation reads all data into memory and performs linear searches. As your knowledge graph grows, queries get slower and slower.
3. No Transaction Support
If a partial update fails, there's no guarantee of data consistency. This is a dealbreaker for production use.
The Solution: SQLite + WAL Mode
I implemented a SQLite-based Memory MCP that solves all these issues.
What Is WAL (Write-Ahead Logging) Mode?
SQLite's WAL mode is genuinely game-changing:
- Concurrent reads and writes: Multiple readers and one writer can access the database 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 features that seems simple but changes everything.
Implementation Details
Database Initialization
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:
-
CASCADE: Automatically delete related data when an entity is removed (no orphaned records) -
UNIQUEconstraints: Prevent duplicate data - Indexes: Dramatically improve query performance
Those indexes are crucial. Without them, queries slow down fast.
Transaction-Based Updates
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 matter:
- 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.
Published as an npm Package
I published this as an npm package so anyone can use it easily.
Installation
npm install @pepk/mcp-memory-sqlite
Configuration for Claude Desktop / Claude Code
Add this 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:
-
MEMORY_DB_PATH: I recommend per-project databases like./.claude/memory.db - Global sharing is possible: set to
~/memory.dbor similar
Per-project databases are easier to manage in practice.
Comparison with Official Implementation
| 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 | Weak | Strong (foreign keys) |
| Crash recovery | ❌ Manual repair | ✅ Automatic (WAL) |
The differences are pretty significant.
Troubleshooting
Error: database is locked
If you still get this with WAL mode enabled:
// Increase the busy timeout
this.db.pragma('busy_timeout = 10000'); // 10 seconds
If 5 seconds isn't enough, try 10.
WAL File Growing Too Large
# Run checkpoint to truncate WAL file
sqlite3 memory.db "PRAGMA wal_checkpoint(TRUNCATE);"
Running this periodically keeps the WAL file size under control.
Summary
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
- Published as an npm package for easy use
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.
Resources
- npm package: @pepk/mcp-memory-sqlite
- GitHub repository: Daichi-Kudo/mcp-memory-sqlite
- SQLite WAL mode: SQLite Write-Ahead Logging
If this helps solve your Claude Code concurrent session problems, I'd love to hear about it! Issues and PRs are always welcome.
Read in Other Languages
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
Top comments (0)