AI/TLDR

LangGraph Persistence: Checkpointers and Memory

Learn how LangGraph's checkpointers persist graph state — enabling pause/resume, human-in-the-loop approvals, time-travel debugging, and memory that survives across sessions.

INTERMEDIATE10 MIN READUPDATED 2026-06-13

In plain English

LangGraph lets you build an agent as a graph: nodes do work, edges decide what runs next, and a shared state object flows between them. By default that state lives only in memory. The moment the run ends — or your server restarts, or the process crashes — the state is gone. The agent has no memory of what just happened.

Persistence & Checkpointers — illustration
Persistence & Checkpointers — frontiersin.org

Persistence fixes that. A checkpointer is a small piece you plug into the graph that saves a snapshot of the state after every step. Each snapshot is a checkpoint. Because the full state is on disk, the graph can stop in the middle, wait for hours or days, and then pick up exactly where it left off — same variables, same position in the graph, as if no time had passed.

Think of a long board game you play across several evenings. When you stop, you photograph the board — every piece, whose turn it is, the score. Next time you set the board back up from the photo and continue. The checkpointer is that photographer; the checkpoint is the photo; and a thread is one ongoing game, kept separate from every other game on the shelf.

Why it matters

Persistence is the line between a demo and a product. A graph with no checkpointer is a one-shot script: it runs start to finish in a single call and forgets everything. Almost every real agent needs to do something that single call can't.

  • Memory across sessions. A user chats today, comes back tomorrow, and the agent still remembers the conversation. You load the same thread_id and the prior messages and state are right there — no need to re-send the whole history yourself.
  • Human-in-the-loop. Before the agent sends an email, charges a card, or merges code, you want a person to approve. Persistence lets the graph pause mid-run, hand control back to your app, and resume later with the human's decision folded in.
  • Crash recovery. A long, multi-step agent (research, then summarize, then file a report) might run for minutes. If step 3 of 5 crashes, you don't restart from scratch — you resume from the last checkpoint, keeping the work already done.
  • Time-travel debugging. Because every step is saved, you can rewind to any past checkpoint, inspect exactly what the state was, change an input, and re-run from there. That makes flaky agent bugs reproducible.
  • Fault-tolerant background jobs. A worker can pick up a thread, advance it a few steps, save, and let another worker continue. State lives in the store, not in one process's memory.

This is also what people mean when they say LangGraph is "production-grade" while a plain LangChain chain is more of a pipeline. The conceptual difference — nodes, edges, and state — is covered in what is LangGraph and LangChain vs LangGraph. The persistence layer described here is what turns those concepts into something you can ship.

How it works

You attach a checkpointer when you compile the graph. After that, invoking the graph requires a thread_id so LangGraph knows which conversation this run belongs to. With those two things in place, the save-and-resume behavior is automatic — you don't call "save" yourself.

The save loop

Each time a node finishes, LangGraph writes a checkpoint: the full state, which node ran, and what comes next (the "next" pointer). On the following invocation with the same thread_id, LangGraph reads the latest checkpoint, restores the state, and continues from the saved position. State updates are reducers applied on top of the previous checkpoint, not blind overwrites — so message lists append rather than replace.

Threads keep runs separate

Every invocation passes a config like {"configurable": {"thread_id": "user-42"}}. All checkpoints for that id form one thread. Use a new id for a fresh conversation; reuse an existing id to continue one. The checkpointer is a single store fanning out to many independent thread histories.

Here is the whole pattern in code. Notice there is no explicit save — compiling with a checkpointer and invoking with a thread_id is all it takes.

persistent_graph.pypython
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import InMemorySaver

def chat_node(state: MessagesState):
    # ... call your LLM, return new messages ...
    return {"messages": [("assistant", "Hi, I remember you.")]}

builder = StateGraph(MessagesState)
builder.add_node("chat", chat_node)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)

# 1) Attach a checkpointer at compile time.
graph = builder.compile(checkpointer=InMemorySaver())

# 2) Every call carries a thread_id.
config = {"configurable": {"thread_id": "user-42"}}

graph.invoke({"messages": [("user", "Hello")]}, config)
# Later — same thread_id — and prior messages are still in state:
graph.invoke({"messages": [("user", "What did I just say?")]}, config)

# Inspect the saved state at any time:
snapshot = graph.get_state(config)
print(snapshot.values)   # the state dict
print(snapshot.next)     # which node would run next

Choosing a checkpointer

LangGraph ships several checkpointer backends. They all expose the same interface, so you can develop against one and deploy against another by swapping a single line. The difference is where the checkpoints are stored and how long they survive.

CheckpointerStored whereSurvives restart?Best for
InMemorySaverProcess RAMNoTests, notebooks, quick demos
SqliteSaverA local .db fileYes (single machine)Local apps, prototypes, single-server tools
PostgresSaverA Postgres databaseYes (shared)Production, multiple servers/workers

The rule of thumb: InMemorySaver for throwaway runs, SqliteSaver when you want persistence on one machine with zero setup, and PostgresSaver once multiple processes must share the same threads. Each has a synchronous and an async variant (for example an async Postgres saver) — pick the one matching how you call the graph.

A worked example: pause for human approval

The most common reason people reach for persistence is human-in-the-loop: stop the agent before a risky action, let a person decide, then continue. Persistence is what makes the pause possible — the state has to be saved while you wait for the human, who might take a minute or a day.

Inside a node you call interrupt(...). That throws a special signal: LangGraph saves a checkpoint and returns control to your app, with the run frozen exactly there. When you're ready, you invoke the graph again on the same thread with a Command(resume=...) carrying the human's answer, and execution continues from the interrupt as if the function never stopped.

human_approval.pypython
from langgraph.types import interrupt, Command

def send_email_node(state):
    draft = state["draft"]
    # Pause and surface the draft to a human. Execution stops here;
    # the checkpointer has saved everything.
    decision = interrupt({"action": "send_email", "draft": draft})
    if decision == "approve":
        actually_send(draft)
        return {"status": "sent"}
    return {"status": "cancelled"}

config = {"configurable": {"thread_id": "ticket-88"}}

# First invocation runs until interrupt(), then returns.
graph.invoke({"draft": "Hi, your refund is approved."}, config)

# ...minutes or days later, after a human clicked Approve in your UI...
graph.invoke(Command(resume="approve"), config)   # picks up where it paused

Time-travel and inspecting history

Because every step is a saved checkpoint, the whole run is a replayable timeline. Three methods give you the controls.

  • get_state(config) — read the current snapshot for a thread: the state values and the next pointer (which node would run next).
  • get_state_history(config) — list every past checkpoint for the thread, newest first. Each entry has a checkpoint_id you can target.
  • update_state(config, values) — edit the saved state by hand, e.g. to fix a bad value before resuming, or to inject a human correction.

To time-travel, grab a checkpoint_id from the history and pass it in the config. The next invocation resumes from that point instead of the latest one — so you can branch the run, try a different input, and compare outcomes without disturbing the original timeline.

time_travel.pypython
# Walk the full history of a thread.
for snap in graph.get_state_history(config):
    print(snap.config["configurable"]["checkpoint_id"], "->", snap.next)

# Pick an earlier checkpoint and resume FROM it (a new branch).
rewind = {"configurable": {
    "thread_id": "user-42",
    "checkpoint_id": "<an-old-checkpoint-id>",
}}
graph.invoke(None, rewind)   # re-runs forward from that saved point

This is the feature that makes agent debugging tractable. Pair it with tracing in LangSmith and you can both see why a run went wrong and replay it from before the mistake.

Going deeper

Once the basics click, a few distinctions and edge cases separate a toy setup from a robust one.

Short-term vs long-term memory. Checkpointers give thread-scoped (short-term) memory: state that belongs to one conversation. They do not automatically share knowledge across threads. For that, LangGraph adds a separate Store (often InMemoryStore in dev, a Postgres-backed store in production) keyed by a namespace and key — use it to remember facts about a user that should persist across all their conversations. Threads forget each other; the Store is how you remember a user between threads.

Database setup. Persistent backends need their tables created once. SQLite and Postgres savers expose a setup() call (or run it for you on first use) that creates the schema. In production, run that migration step as part of deploy rather than relying on first-request creation, so the very first user doesn't race the schema build.

State growth and serialization. Every checkpoint stores the whole state. If your state holds a giant message history or large blobs, checkpoints get heavy and writes slow down. Keep state lean: store IDs or references, not megabytes of raw text; trim or summarize long message lists; and remember that whatever you put in state must be serializable to be saved.

LangGraph Platform. The hosted/deployment layer builds on exactly this persistence model — it manages a Postgres-backed checkpointer, threads, and the Store for you, and exposes them over an API, so you don't operate the database yourself. The mental model you've learned here is the same; the platform just runs it.

Where to go next. Persistence is one pillar of why teams pick LangGraph over a hand-rolled loop; the broader tradeoffs live in agent framework comparison and choosing an agent framework. If you're weighing whether you even need a framework's persistence at all, building an agent without a framework is a useful contrast — you'll find yourself reinventing exactly the checkpoint loop described above.

FAQ

What is a checkpointer in LangGraph?

A checkpointer is the component that saves a snapshot (a checkpoint) of your graph's state after every step. You attach one when compiling the graph. With a checkpointer in place, a run can pause, survive a restart, and resume from exactly where it stopped. Common backends are InMemorySaver, SqliteSaver, and PostgresSaver.

What is a thread_id in LangGraph and why do I need it?

A thread_id identifies one conversation or run. All checkpoints for the same id form a single thread with its own independent history. You pass it in the config ({"configurable": {"thread_id": "..."}}) on every invocation: reuse an id to continue a conversation, use a new id to start a fresh one.

How does LangGraph remember conversations across sessions?

Persistence handles it automatically. When a user returns, you invoke the graph with their existing thread_id, and LangGraph loads the latest checkpoint — including prior messages — so the agent continues with full context. You don't re-send the history yourself; you just point at the same thread.

Which checkpointer should I use in production?

Use PostgresSaver (or its async variant) when multiple servers or background workers must share the same threads. SqliteSaver is fine for a single-machine app, and InMemorySaver is only for tests and demos because it loses everything on restart.

How do human-in-the-loop approvals work with persistence?

Inside a node you call interrupt(...), which saves a checkpoint and returns control to your app with the run frozen. After a human decides, you invoke the graph again on the same thread with Command(resume=...), and it continues from the interrupt. The pause holds no process or memory — any worker can resume it later.

Do checkpointers give long-term memory across different users or conversations?

No. Checkpointers give thread-scoped (short-term) memory — they don't share state between threads. For cross-thread memory, like remembering facts about a user across all their conversations, use LangGraph's separate Store, which is keyed by namespace and key.

Further reading