Letta, Mem0, and LangGraph checkpointers all get called "agent memory," but they put the decision to remember or forget in three different places — an agent runtime, an external service, or your own application code.
Ask three teams what framework they use for "agent memory" and you'll get three answers that aren't actually comparable: Letta, Mem0, and "LangGraph's checkpointer." That's not a naming inconsistency — those three things solve different parts of the problem, sit at different layers of the stack, and make different bets about who decides what an agent forgets. Treating them as interchangeable is how teams end up bolting a full agent runtime onto a system that just needed a Postgres table, or hand-rolling a consolidation pipeline that a memory service already ships.
This post walks through what each one actually is architecturally, where the boundaries are, and which problem each was built to solve — so "which memory framework should we use" stops being a vibes-based decision.
Before comparing systems, it helps to separate what "agent memory" is actually standing in for. Most production agent memory designs — regardless of vendor — decompose into four tiers borrowed loosely from cognitive science:
┌─────────────────────────────────────────────────────────┐
│ Working memory │
│ (context window — request-scoped, bounded) │
└───────────────────────┬───────────────────────────────────┘
│ write-out at end of turn
▼
┌────────────────────────────────┐
│ Episodic │ "what happened"
│ session/turn history, time- │
│ ordered, retrieved by recency │
└────────────────┬─────────────────┘
│ extraction / consolidation
▼
┌────────────────────────────────┐
│ Semantic │ "what's true"
│ durable facts, preferences, │
│ entities — overwritten, not │
│ appended │
└────────────────┬─────────────────┘
│ (rarely automated yet)
▼
┌────────────────────────────────┐
│ Procedural │ "what works"
│ learned tool sequences, style │
└────────────────────────────────┘
The reason Letta, Mem0, and LangGraph feel hard to compare is that each one draws the boundary between these tiers in a different place, and assigns the decision of what to promote, decay, or discard to a different actor: the agent itself, a background service, or your application code.
Letta started life as MemGPT, a research project built on a specific analogy: treat the LLM's context window like RAM, and give the model explicit system calls to page information in and out of a larger, slower store — the same way an operating system manages virtual memory. That analogy is still the architecture, not just the pitch.
In practice, a Letta agent has two memory regions: core memory, always resident in the context window and editable by the model itself via tool calls, and archival memory, an unbounded store backed by Postgres + pgvector that the agent searches on demand. The critical detail is who moves data between them — it's the agent, via explicit memory_insert, memory_replace, archival_memory_search style tool calls it decides to make mid-conversation. Consolidation is agent-driven, not a background cron job.
That design has real consequences. Because Letta owns the reasoning loop, memory management, and persistence together, it isn't something you drop into an existing LangGraph or CrewAI agent as a plugin — adopting Letta means adopting it as your agent runtime. Recent additions push the same philosophy further: "sleep-time compute" lets an agent reorganize and reflect on its memory during idle cycles instead of only at inference time, and "context repositories" apply git-style versioning to memory state, treating a memory edit like a commit you can diff and roll back.
The tradeoff is infrastructure and lock-in: you're running Postgres (or accepting Letta's hosted version), and you're writing your agent logic inside Letta's execution model rather than composing it into a stack you already have.
Mem0 makes the opposite bet: it doesn't want to be your agent framework at all. It's a memory service your agent calls at two points in every turn — once to retrieve relevant context before reasoning, once to write back anything worth keeping after.
Architecturally, Mem0 runs a two-stage pipeline on every interaction. First, an LLM call extracts candidate facts from the new exchange. Second, a decision step compares those candidates against existing memory and issues one of four operations — ADD, UPDATE, DELETE, or NOOP — so a new statement that contradicts a stored fact overwrites it instead of just appending a duplicate. A graph-backed variant stores entities and relationships instead of flat text, useful when the thing worth remembering is "user's manager is Alice" rather than a sentence.
# Roughly what a Mem0-backed agent turn looks like
memories = mem0.search(query=user_message, user_id=user_id)
response = agent.run(user_message, context=memories)
mem0.add([user_message, response], user_id=user_id) # extraction + ADD/UPDATE/DELETE happens here
Because Mem0 is framework-agnostic by design, it slots into LangChain, LlamaIndex, a raw OpenAI/Anthropic tool-calling loop, or a multi-agent system where several agents share one memory store — a research agent's findings become visible to a drafting agent without either one knowing the other exists. The cost is that Mem0 doesn't give you a reasoning loop or an execution model; you're still responsible for orchestration, and you're trusting its extraction LLM calls to decide what's worth keeping, which adds latency and another point where a bad extraction silently corrupts what the agent believes about a user.
LangGraph refuses to ship an opinionated memory policy at all — it ships two low-level primitives and makes you decide the policy yourself.
Checkpointers persist a thread's full graph state after every node execution — not just at the end of a run. Pass a thread_id and a backend (MemorySaver for dev, PostgresSaver for production, which supports multiple workers reading and writing the same thread for horizontal scaling), and LangGraph handles the save/restore automatically. This is short-term, thread-scoped memory: conversation continuity, time-travel debugging, and fault tolerance, with zero code in your nodes.
Stores are the cross-thread counterpart — a BaseStore abstraction for facts that need to outlive a single conversation: preferences, account history, anything a different thread from the same user should see. Unlike checkpointing, stores are not automatic. You call store.search() and store.put() explicitly, typically in a pre-model hook (inject relevant memories into the prompt before the LLM call) and a post-model hook (write back what's worth keeping after). LangGraph's own docs are explicit that this asymmetry is deliberate: conversation history is structural and the same for every app, but long-term memory is a product decision — what counts as worth remembering varies enough between applications that automating it would mean guessing wrong most of the time.
This is also why LangChain deprecated its older ConversationBufferMemory abstraction in favor of this split — it collapsed two different lifetimes into one object and got the automation boundary wrong. In production, the store is frequently backed by something else entirely — Mem0, LangMem, or Zep plugged in as the storage/retrieval layer behind LangGraph's interface, rather than LangGraph's in-memory default.
| Letta (MemGPT) | Mem0 | LangGraph (checkpointer + store) | |
|---|---|---|---|
| What it is | Full agent runtime with memory built in | External memory service, framework-agnostic | Two persistence primitives inside an orchestration framework |
| Who decides what's remembered | The agent itself, via tool calls | Mem0's extraction/decision pipeline (ADD/UPDATE/DELETE/NOOP) | Your application code, explicitly |
| Automation level | High — agent-driven paging and consolidation | High — background extraction on every turn | Low by design — checkpointing is automatic, store writes are manual |
| Storage backend | Postgres + pgvector (or Letta's hosted service) | Mem0-managed (vector + optional graph store) | Pluggable — in-memory, Postgres, SQLite, or a third-party store |
| Integration model | Replaces your agent loop | Called from within any existing agent loop | Native to LangGraph; other systems can sit behind the Store interface |
| Best fit | Building an agent whose defining feature is long-term, self-managed memory | Adding persistent memory to an existing stack without adopting a new runtime | Already on LangGraph and want control over exactly what gets promoted to long-term memory |
| Main cost | Infrastructure + framework lock-in | Extra LLM calls per turn; less control over extraction quality | You build the retrieval/writeback policy yourself |
The table makes these look like three competing products, but the more useful framing is that they answer a prior question differently: where should the decision to forget or remember something be made? Letta puts it inside the agent's own reasoning, as an action it takes deliberately. Mem0 puts it in a dedicated service that runs the same extraction logic on every interaction regardless of which agent called it. LangGraph puts it in your application code, on the theory that "what's worth remembering" is domain-specific enough that no default will be right often enough to trust.
That means the real question isn't "which is best" — it's which actor in your system you trust to make that call, and whether you're building an agent runtime from scratch (Letta is a legitimate default) or bolting persistence onto orchestration you already have (Mem0 or a LangGraph store, often the two combined, are the more common production pattern). If you're exposing an agent to tools over MCP, this also determines whether "remember this" is a tool call the model invokes explicitly or a side effect that happens transparently after the response — worth deciding on purpose rather than by whichever framework you happened to reach for first.
Takeaway: don't ask "what memory framework should we use" — ask "who decides what gets promoted from working memory to something durable," then pick the system that puts that decision where you actually want it.