Mem0 vs Zep vs Letta: The Production Cost of Agent Memory
Why We Compared Mem0, Zep/Graphiti, and Letta
We did not start this review because our agents had forgotten a favorite color. We started it because memory had entered the synchronous request path.
Once an agent searches long-term memory before every model call, retrieval latency becomes user-visible. Once it extracts memories after every response, ingestion becomes both a latency and model-cost problem. Once memories can be corrected or deleted, read-after-write behavior and provenance stop being optional details.
We evaluated three materially different architectures:
- Mem0 is a dedicated memory layer. It extracts durable facts from messages and retrieves them through a memory-focused API.
- Zep is now a managed product built around temporal knowledge-graph concepts. Its open-source foundation is Graphiti, not the deprecated Zep Community Edition.
- Letta, formerly MemGPT, is a stateful agent runtime. Memory is part of the agent abstraction rather than an independent retrieval service.
That distinction matters. Comparing these systems as interchangeable vector databases produces a misleading benchmark.
Mem0 places extraction and retrieval policy around a vector store. Its April 2026 algorithm moved to single-pass, ADD-only extraction: one model call, no UPDATE or DELETE during extraction, and accumulated memories rather than overwritten facts. That simplifies the hot write path, but it shifts conflict resolution into retrieval and application policy.
Graphiti models episodes, entities, relationships, and time. It can represent that Alice worked at Company A before Company B instead of storing two unrelated text fragments. We pay for that structure through extraction, entity resolution, graph writes, and more operational machinery.
Letta keeps state close to the running agent. Its memory blocks and conversation state are useful when the agent itself owns the lifecycle. They are less natural when several services need a neutral, portable memory API.
We therefore framed the decision around four production questions:
- How long does a memory write keep the turn open?
- When does a completed write become searchable?
- How much context does retrieval inject into every future inference?
- What infrastructure and model costs should we expect at 10,000 users?
We also refused to manufacture a clean local leaderboard. We could not complete our planned package-pinned reproduction without guessing current APIs and substituting mocked vendor behavior. In particular, the current Zep repository is an examples-and-integrations repository for Zep Cloud, while Letta’s active implementation has moved to letta-ai/letta-code. Those are product-boundary changes, not trivial installation issues.
Instead, we combined verified installation paths, a reproducible benchmark design, and a tightly scoped 419-turn reference run. We retained its original conditions rather than presenting public-internet and local-process measurements as equivalent.
For related infrastructure evaluations, browse our tools collection.
Hands-On Walkthrough: Setup, Execution & Output
We began with the official installation surfaces.
For Mem0’s Python library:
python -m venv .venv
source .venv/bin/activate
pip install mem0ai
# Optional hybrid-search and entity-processing support
pip install "mem0ai[nlp]"
python -m spacy download en_core_web_sm
The official self-hosted route is:
git clone https://github.com/mem0ai/mem0.git
cd mem0/server
# Recommended bootstrap path
make bootstrap
# Manual alternative
docker compose up -d
Mem0 self-hosted authentication is enabled by default. For a real deployment, we set ADMIN_API_KEY or complete the administrator wizard. AUTH_DISABLED=true belongs only in an isolated local environment.
The core Python interface is straightforward:
from mem0 import Memory
memory = Memory()
memory.add(
[
{"role": "user", "content": "I prefer invoices as PDF files."},
{"role": "assistant", "content": "I will use PDF for future invoices."},
],
user_id="tenant-a:user-42",
)
result = memory.search(
query="How should invoices be delivered?",
filters={"user_id": "tenant-a:user-42"},
top_k=3,
)
For Zep Cloud, the official SDK installations are:
pip install zep-cloud
npm install @getzep/zep-cloud
go get github.com/getzep/zep-go/v3
Those commands do not create a self-hosted Zep product. The old Community Edition is deprecated and unsupported. For a local temporal graph, we instead have to test Graphiti and own its graph database, extraction model, embeddings, migrations, authorization, and service wrapper.
For our proposed Graphiti adapter, we would ingest episodes and then search the graph. The following sketch is unverified against a pinned release; we did not execute it or validate its API signatures:
from datetime import datetime, timezone
from graphiti_core import Graphiti
from graphiti_core.nodes import EpisodeType
graphiti = Graphiti("bolt://localhost:7687", "neo4j", "benchmark-password")
await graphiti.build_indices_and_constraints()
await graphiti.add_episode(
name="support-turn-0001",
episode_body="Alice changed invoice delivery from email text to PDF.",
source=EpisodeType.text,
source_description="Synthetic support conversation",
reference_time=datetime.now(timezone.utc),
)
results = await graphiti.search("How should Alice receive invoices?")
For Letta’s current runtime:
npm install -g @letta-ai/letta-code
# Interactive terminal
letta
# Local application server
letta server
We do not treat Letta as an add() and search() library. Our adapter writes through an agent message or memory-block operation, then reads the resulting agent state and archival recall exposed by that server version. Pinning the generated client and server together is essential because the historical V1 API server now lives on an archive branch.
Our neutral baseline uses Postgres with pgvector. Neo4j is optional and only required for the Graphiti path:
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: memory
POSTGRES_PASSWORD: memory
POSTGRES_DB: memory_bench
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U memory -d memory_bench"]
interval: 2s
timeout: 2s
retries: 30
neo4j:
image: neo4j:5
profiles: ["graph"]
environment:
NEO4J_AUTH: neo4j/benchmark-password
ports:
- "7474:7474"
- "7687:7687"
Our benchmark contract separates ingestion, visibility, retrieval, and answer quality:
- Generate 10,000 deterministic user IDs.
- Ingest 200 timestamped memories per user.
- Include corrections, superseded preferences, multi-hop relationships, and deliberately unanswerable questions.
- Record write-return latency independently from background processing.
- Poll search after every write to measure visibility lag.
- Retrieve the same maximum number of items from each adapter.
- Feed only retrieved text into one shared answering model at temperature zero.
- Record retrieved tokens, strict recall, judged recall, and correct abstentions.
- Measure database bytes after vacuuming or compaction.
- Run cold, warm, and 32-concurrent-client phases separately.
Our proposed harness would validate configuration in dry-run mode before touching a model endpoint. The following is simulated representative output, not an executed validation or a vendor benchmark:
$ python bench.py validate --users 10000 --memories-per-user 200
[ok] workload schema: v1
[ok] users: 10000
[ok] planned memory events: 2000000
[ok] correction events: 200000
[ok] unanswerable probes: 2800
[ok] deterministic embedding adapter: configured
[ok] postgres/pgvector: reachable
[skip] neo4j/graphiti: profile not enabled
[blocked] zep-cloud: API key not supplied
[blocked] letta: server/client version pair not pinned
[blocked] mem0 extraction: deterministic local model endpoint not supplied
No latency results emitted: validation mode performs no timed service calls.
That last line is important. A harness that silently skips an adapter can generate a polished but worthless comparison.
Setup Problems and Consistency Limitations
Our first setup problem was identifying the supported product.
Cloning getzep/zep no longer gives us a supported self-hosted Zep server. It gives us examples, integrations, ingestion tools, benchmarks, and legacy code. A local Zep-like deployment means Graphiti plus infrastructure we assemble ourselves. Any article presenting docker compose up against the old Community Edition as current Zep is benchmarking an unsupported product.
The second setup problem was Letta’s repository transition. The letta-ai/letta repository points active development to letta-ai/letta-code; historical V1 tags remain available for reproducibility. We could either benchmark an old API with stable instructions or benchmark the current runtime with a version-pinned client/server pair. Mixing those paths would invalidate both setup and latency numbers.
In the historical Letta server setup, we encountered a Postgres requirement and write problems on the older SQLite path. We would not treat that SQLite release as a production workaround. These findings do not establish the database requirements of the current letta-code App Server; we would verify those separately before choosing a deployment configuration.
Graphiti introduced a different failure class. Its Kuzu path attempted searches against indices that had not been created in the affected setup. We would not conceal that setup failure by excluding index-build time. The correct sequence is to create and verify indices and constraints explicitly, fail startup if they are absent, and then benchmark ingestion. For production, we prefer Neo4j over an under-tested embedded fallback when temporal graph behavior is central to the product.
Mem0’s friction was less about installation and more about hidden work. memory.add() is not equivalent to inserting one row. It can invoke an extraction model, parse facts, embed them, and write them to the configured store. In the 419-turn reference configuration, Mem0 2.1.0 spent 1.52 seconds per ingested turn while search reached a 38 ms median locally.
That run used the same 33,653-character extraction prompt, estimated at roughly 8,478 tokens, on every turn. Across 419 turns, repeated prompt material drove input usage toward ten million tokens before the question phase. Prompt caching can recover much of that with a compatible provider. Without caching, the write-side model bill can dominate storage.
Consistency also needs explicit testing. An API returning 200 OK can mean:
- the request was accepted;
- the raw event was committed;
- extraction finished;
- embeddings were stored;
- graph entities were resolved; or
- the memory is already retrievable.
Those states are not interchangeable. We require adapters to expose a job identifier or a visibility barrier for asynchronous writes. If neither exists, our harness polls by a unique marker and records write-to-search delay. For user-facing corrections, we keep authoritative profile fields in Postgres and treat semantic memory as a secondary retrieval layer.
Finally, ADD-only extraction avoids destructive mutations but does not solve contradiction. “Alice prefers email” and “Alice now prefers PDF” can coexist. We therefore include event time, ingestion time, source turn, tenant, user, and supersession metadata. Retrieval must prefer current evidence without deleting the audit trail.
Scale, Latency & Cost vs. Alternatives
The most useful reference run used one 419-turn LoCoMo conversation, 92 answerable probes, and 28 adversarial probes. Every system’s retrieved text went to the same answering model. That isolates retrieval better than allowing each product to answer with its own agent.
The latency topology was not uniform: Mem0 ran with local Qdrant, Zep/Graphiti and Letta used their tested local paths, while other products in the broader run included hosted network latency. We therefore treat these figures as directional, not as universal service-level objectives.
| System and tested version | Architecture | Search p50 | Ingest per turn | Retrieved context | Judge recall | Operational reading |
|---|---|---|---|---|---|---|
| Mem0 2.1.0 | Extracted facts plus local vector retrieval | 38 ms | 1.52 s | 353 tokens | 50.0% | Fast read path; extraction cost moves to writes |
| Zep/Graphiti 0.30.2 | Temporal knowledge graph | 163 ms | 3.65 s | 212 tokens | 37.0% | Smallest context; highest graph and extraction burden here |
| Letta 0.11.7 | Stateful agent with verbatim memory | 318 ms | 0.37 s | 503 tokens | 52.2% | Better recall in this run; slower search and tighter runtime coupling |
| DIY pgvector | Application-owned chunks and metadata | Not measured in the reference run | No extraction unless added | Controlled by application | Workload-dependent | Application-owned control plane and memory policy; comparative cost not measured |
We keep the April 2026 managed-platform results separate from the local-search comparison: 0.88 seconds p50 on LoCoMo, a top-200 single-pass retrieval budget, 7.0K tokens, and a 92.5 score. That managed path includes proprietary optimizations unavailable in the open-source SDK. We do not equate its reported latency with the 38 ms local search measurement or assume identical timing boundaries.
The quality result also needs restraint. On the fixed 419-turn run, Letta reached 52.2%, Mem0 50.0%, and Zep/Graphiti 37.0% under the shared-answer-model method. Every system answered fewer than half of the multi-hop questions. That tells us these products remain retrieval components, not sources of truth.
For storage planning, our baseline calculation is more actionable than a vague “vector databases are cheap.”
Assume:
- 10,000 users;
- 200 retained memories per user;
- 1 KB of text and metadata per memory;
- one 1,536-dimensional float32 embedding;
- two million total memory records.
Raw text and metadata consume roughly 2 GB before database overhead. Each vector is 6,144 bytes, so vectors alone consume about 12.3 GB. Tables, row metadata, write-ahead logs, backups, and HNSW or IVFFlat indexes push the practical footprint higher. A three-copy operational layout can easily turn a roughly 15–25 GB primary dataset into 45–75 GB of provisioned storage.
Graph memory adds nodes, relationships, properties, graph indexes, and extracted episode data. Its break-even point is not storage price alone. We would choose it only when temporal relationship quality saves enough model calls, manual review, or failed tasks to offset:
graph premium =
graph database cost
+ extraction model cost
+ entity-resolution cost
+ additional operations time
- context-token savings
- task-failure savings
Zep/Graphiti’s 212 retrieved tokens were attractive against Mem0’s 353 and Letta’s 503. At large query volume, that standing context cost matters. At 100 requests per user per month and 10,000 users, every extra 300 retrieved tokens becomes 300 million additional model-input tokens monthly.
Our practical break-even rule is:
- Start with pgvector when memory is mostly isolated preferences, summaries, and searchable events.
- Add Mem0 when extraction and memory lifecycle improve recall enough to justify write-side model calls.
- Add Graphiti when temporal relationships are product-critical, not merely interesting.
- Use Letta when we want its stateful agent runtime as well as its memory model.
If your workload or compliance boundary does not fit those categories, talk to our infrastructure team before committing the memory layer to every request.
Our Final Verdict: When to Deploy, When to Skip
There is no universal winner because these products solve different layers of the system.
We would deploy Mem0 if:
- we need a clean memory-focused API rather than a complete agent runtime;
- low retrieval latency matters more than synchronous write latency;
- we can cache or tightly control extraction-model prompts;
- accumulated facts are acceptable with application-level conflict handling;
- and we are prepared to test open-source behavior independently from managed-platform benchmarks.
We would deploy Zep or Graphiti if:
- temporal entities and changing relationships are central to the task;
- a 200-token graph result is more valuable than a larger semantically similar context;
- we can operate Neo4j or buy the managed service;
- and we can tolerate slower ingestion while graph extraction and entity resolution complete.
We would not select Zep merely because an old Community Edition Compose file exists. That route is deprecated.
We would deploy Letta if:
- the agent runtime, identity, tools, conversations, and memory should share one state model;
- memory blocks are the desired abstraction;
- Postgres is already part of the architecture;
- and portability to an independent retrieval service is not a near-term requirement.
We would hold off on all three if:
- memory must be immediately and strongly consistent after every write;
- the stored value is authoritative account or transaction state;
- cross-tenant deletion cannot be independently verified;
- multi-hop recall must be reliably above the roughly half-correct range seen here;
- or the team has not measured context tokens and extraction calls separately.
Our default production architecture remains conservative: authoritative state in Postgres, live facts behind tools, documents in RAG, and agent memory limited to scoped preferences, summaries, and prior decisions. We add a specialized memory product only when it beats that baseline on our workload.
Mem0 is the easiest of the three to introduce as a dedicated layer. Letta is the strongest fit when we are deliberately buying into a stateful agent runtime. Zep/Graphiti offers the richest temporal model but also creates the largest operational commitment.
We therefore choose based on consistency, extraction cost, and failure behavior—not raw retrieval latency alone. We must be able to explain those trade-offs during an incident and keep retrieved memory separate from authoritative state.
Get the next one
in your inbox.
One short weekly dispatch with new guides, tools, and what we tested. No spam, unsubscribe anytime.
Get weekly AI tool reviews & automation tips
Join our newsletter. No spam, unsubscribe anytime.
More in Articles
We tested Google A2A 1.0 between local Python agents, including discovery, task lifecycle, artifacts, authentication, and migration from pre-1.0 agent cards. Here is what worked, what broke, and where an adapter layer remains necessary.
We deployed Langfuse and Arize Phoenix locally, pushed synthetic LLM traces through their OpenTelemetry interfaces, exercised evaluation workflows, and measured the operational cost hidden behind self-hosting.
We deployed Pydantic AI, exercised structured outputs, dependency injection, retries, and tool calls, then compared its behavior with LangGraph across a 160-scenario test matrix.
We benchmarked DSPy GEPA against manually engineered prompts, measuring held-out accuracy, optimization token cost, latency, and transfer to a newer model.