Fixing Claude Code's Concurrent Session Problem: Implementing Memory MCP with SQLite WAL Mode

Published: (January 1, 2026 at 03:55 AM EST)
5 min read
Source: Dev.to

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:

FeatureWhat WAL gives you
Concurrent reads/writesMultiple readers + one writer can access the DB simultaneously
Crash recoveryData integrity is guaranteed even if the process crashes mid‑write
Faster writesLog‑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

PrincipleWhy it matters
CASCADEAutomatically delete related data when an entity is removed (no orphaned records).
UNIQUE constraintsPrevent duplicate data.
IndexesDramatically 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

VariableRecommendation
MEMORY_DB_PATHPer‑project databases like ./.claude/memory.db are easier to manage.
Global sharingSet to ~/memory.db (or similar) if you need a single DB for all projects.

Feature comparison

FeatureOfficial (JSONL)This Implementation (SQLite + WAL)
Concurrent access❌ Not supported✅ Supported (WAL)
Transactions❌ None✅ ACID guarantees
Search speedSlow (linear)Fast (indexed)

Data Integrity

LevelDescription
Weak
Strong(foreign keys)

Crash Recovery

FeatureStatus
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.

Back to Blog

Related posts

Read more »

A Eficiência do Cache no SQLite

A Eficiência do Cache no SQLite !Cover image for A Eficiência do Cache no SQLitehttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=aut...