Skip to content
Concurrency Models for Shared Agent Memory

Concurrency Models for Shared Agent Memory

Tier 1’s narrow lock works because its actual concurrency is low enough that a five-line mkdir mutex can solve it directly and correctly. That approach stops working somewhere between tens and low hundreds of concurrent writers — not because locks are conceptually wrong, but because a lock held even briefly starts to matter when enough processes are queued behind it. Tier 2 and tier 3 need a real answer to a specific mechanical question: when two writers hit the same piece of shared state at the same instant, what actually happens?

Four systems that face this question for real were checked directly against their own source code and issue trackers, not their marketing pages, specifically to answer that one mechanical question. The results are more varied, and more informative, than “production AI memory has this solved.”

Four systems, each as a two-layer stack showing where the real guarantee stops — Mem0 unsafe throughout, Letta’s rewrite operation unsafe by its own admission, Graphiti’s storage layer safe with a fixed incident above it, LangGraph’s database layer safe but its scheduler still racing

Mem0: no concurrency control, and a filed bug that proves it

Mem0’s own paper describes its write pipeline as strictly sequential, per message: extract candidate facts, search for similar existing memories, let an LLM tool call decide whether to add, update, delete, or do nothing, then write. The architecture description contains zero mentions of concurrency, locking, or transactions anywhere — not an omitted section, a genuinely single-writer design.

That gap isn’t theoretical. A filed bug report against Mem0’s own repository — titled, with admirable precision, “hash-dedup TOCTOU race in add() creates permanent duplicate memories under concurrency” — documents exactly the failure this predicts. Memory.add() snapshots existing memories early via a vector-store search, then checks a deduplication guard against that same now-stale snapshot several LLM round-trips later. Two concurrent calls to the same scope both pass the dedup check against identical stale state, and both insert — producing permanent duplicate rows, same content, same hash, different IDs. The TypeScript SDK has the identical bug, described there explicitly as a “purely-async TOCTOU, not a thread race,” since Node is single-threaded and the actual race window is the gaps between await calls. Worth being precise about status rather than rounding up: the proposed fix — a per-session-scope async mutex, an application-level patch bolted on after the fact rather than a redesign of the storage layer — has three approving reviews from a contributor but, as of this writing, is still open and unmerged, and no maintainer has commented on the issue itself. Filed and under review is the accurate claim; “fixed” isn’t yet. Mem0’s scoping mechanism (isolating different users, agents, and sessions from each other) does real work, but it does nothing for two writers hitting the same scope, which is precisely what broke.

Letta: the only system that tells you the truth up front

Letta (formerly MemGPT) is the one system in this survey that documents its concurrency limits explicitly, operation by operation, rather than leaving them to be discovered via a bug report. Its own docs publish a table: appending to a memory block is safe under concurrency by construction — an append-only operation has nothing to race. A targeted string replace is conditionally safe: it fails cleanly if the target string has already changed, an optimistic check rather than a lock. A full block rewrite is flatly unsafe — last-writer-wins — with an explicit warning that multiple agents rewriting the same block simultaneously leads to lost updates.

The mitigation Letta documents for that third case isn’t mechanical at all. It’s architectural: designate one agent as the “owner” of a block for full rewrites, and restrict every other agent to append-only inserts. That’s a real answer, honestly stated, but it’s a process discipline enforced by whoever designs the agent roles, not a guarantee the system provides on its own. Worth flagging directly: a widely-repeated claim, surfaced through AI-generated documentation aggregators rather than Letta’s own source, holds that Letta’s database layer uses a version column for optimistic locking. That claim could not be verified — the current letta-ai/letta repository’s main branch contains no letta/ source directory at all as of this research, consistent with a restructuring around a newer SDK. Treat the version-column claim as unconfirmed, possibly describing a codebase that no longer exists, and trust only what’s actually documented: append is safe, replace is conditionally safe, rewrite needs a human-assigned owner.

Zep/Graphiti: a real incident, a real fix, and a boundary that’s easy to miss

Graphiti, the memory engine behind Zep, genuinely uses ACID transactions per operation against its Neo4j backend — this is directly visible in its driver source, not an inferred property. It also has the most mature incident-response history of the four systems checked here, which is worth taking as a point in its favor rather than a mark against it: a real, filed, publicly-tracked bug reported nineteen misplaced episodes scattered across five different graphs in production, with no crash and no error — a silent correctness failure, the worst kind. That specific incident was observed on Graphiti’s FalkorDB backend, not the Neo4j backend just described — the underlying flaw was shared-architecture-wide and got fixed for both, but the 19-episode incident itself didn’t hit the ACID-transactional path above. The root cause, documented directly in the current source’s own code comments: the system historically reassigned a single shared database-driver object whenever a request needed to target a different graph, and because the surrounding code has many await points — LLM calls, embedding calls, database writes — a concurrent request for a different graph could reassign that shared driver mid-execution, silently redirecting the first request’s remaining writes to the wrong destination.

The fix — giving each call its own driver instance instead of mutating one shared object — is real, merged, and shipped. But it’s worth being precise about exactly what it fixes: driver routing. True write-ordering within a single scope still depends on an optional, separate queue service that processes one scope’s writes strictly sequentially while letting different scopes run concurrently — application-level serialization layered on top of the core library, not something the core library itself guarantees. Anyone calling Graphiti’s core library directly, without that optional queue, has no ordering guarantee across concurrent writes to the same scope beyond whatever the underlying database’s own transaction isolation happens to provide.

LangGraph: a real database guarantee sitting above a real, open race

LangGraph’s Postgres-backed persistence layer does something genuinely correct: concurrent writers racing to save the same checkpoint get serialized atomically by an ON CONFLICT ... DO UPDATE clause, handled entirely inside the database, no corruption possible even under a true race. It’s a real, verifiable guarantee, visible directly in the SQL. LangGraph’s own documentation is explicit that this matters — the in-memory store is positioned as development-only, with the Postgres-backed store recommended specifically for production use and “multi-user concurrency.”

But the database layer isn’t the whole path a write travels, and that turns out to matter. An open issue against LangGraph’s core scheduler — filed, and as of this writing still open with active discussion, a proposed lock-based fix already closed without merging — documents a race in the in-process code that assembles pending writes before they ever reach that safe Postgres upsert. A list shared between the main thread and background worker threads gets mutated through a read-filter-reassign-extend pattern that isn’t atomic, and concurrent threads can race on it, silently dropping each other’s pending writes before the database ever sees them. The database-transactional guarantee is completely real. It simply doesn’t extend as far back in the pipeline as it looks like it should, and the gap is exactly the kind of thing that’s invisible from the outside — the correct-looking part of the system is genuinely correct, which is what makes the earlier, broken part easy to overlook.

What none of them do

Checked explicitly, across all four systems’ documentation and source: none of them use CRDTs. None of them use event-sourcing or append-only log ingestion as their concurrency-control mechanism. This is worth sitting with rather than skimming past, because CRDTs are the answer a distributed-systems background reaches for by reflex — conflict-free replicated data types are specifically designed to let independent writers make progress without coordinating and still converge to a consistent result, which sounds like exactly the property a shared agent-memory system wants. Mem0’s write pipeline is a direct read-modify-write, structurally the opposite of an append-only log. Letta’s “append-only” insert operation is a convenience property of one block type, not a system-wide design, and a full rewrite destroys prior state outright rather than layering a new version onto a log. Graphiti’s soft-delete, invalidate-don’t-discard edge semantics look superficially CRDT-adjacent but are actually built for temporal reasoning — tracking what was true when — not for resolving concurrent write conflicts; no log-based ingestion pipeline exists anywhere in the core library. LangGraph’s checkpoint chain, with each checkpoint pointing at its parent, is the closest thing to an append-mostly history among the four, and even that allows a given checkpoint to be overwritten via the same conflict clause that makes it safe, which disqualifies it as a strict event log.

The absence, consistent across every system checked, is itself the finding. None of the production or near-production systems most likely to need CRDTs or event sourcing have reached for either. They reach for whatever guarantee the underlying general-purpose database already provides — Postgres’s transactional semantics, Neo4j’s ACID transactions — or, when nothing underneath provides one, they don’t solve the problem and it surfaces later as a filed bug with someone’s name on the report.

What this means for tiers 2 and 3

Ranked honestly from weakest to strongest concurrency story: Mem0 has none, confirmed by matching bug reports in two separate SDKs with the same root cause. Letta discloses its limits with unusual honesty but solves them through human-assigned process discipline rather than a mechanical guarantee. Graphiti has the most mature incident-response trail — a real corruption bug, a real fix — but true ordering for a single scope still depends on an optional add-on, not the core library. LangGraph is the only one of the four with a directly-verifiable database-transactional guarantee, and it still sits above an open, unresolved race in the layer that feeds it.

The pattern across all four is more useful than the ranking: nobody invented novel concurrency machinery specific to the domain of “agent memory.” Every real guarantee anywhere in this survey is borrowed wholesale from a general-purpose transactional database — Postgres or Neo4j, doing exactly what they’d do for any other application. Where a system skipped borrowing that guarantee, it paid for the omission in a filed bug, not a theoretical concern. That’s the actual design lesson for tiers 2 and 3 of this series’ own architecture: don’t invent a bespoke concurrency model for agent memory specifically. Borrow a transactional guarantee deliberately, know precisely how far up the stack it extends — LangGraph’s own gap shows a correct database layer can still sit under a broken scheduler — and treat every point where application code touches shared state outside that guarantee as a candidate for the same class of lost-update bug a naive read-modify-write invites at any scale, from a single-digit-writer tool to a production fleet.

One more shape worth naming before moving on: every writer in every system surveyed here is an agent, or an LLM-driven pipeline acting like one. None of these four systems’ designs anticipate a writer that isn’t running a language model at all — a regression suite, a telemetry pipeline, a human filing a correction by hand. That’s not a gap in the survey. It’s a gap in the field, and it’s exactly where the next article goes.

Last updated on