AI agents
Ren Okabe8 min read10 views

LangGraph Checkpointer: Persist and Resume Agent State (2026)

Make a LangGraph agent durable with a checkpointer: attach SqliteSaver in one line, crash the graph on purpose, and resume from disk without re-running the Claude call.

Rows of dark server racks with blue indicator lights, representing durable checkpoint storage
Rows of dark server racks with blue indicator lights, representing durable checkpoint storage
On this page

Quick answer (2026)

A LangGraph checkpointer saves your graph's state after every step, so an agent can pause, survive a crash, and resume exactly where it stopped instead of starting over. You attach one with builder.compile(checkpointer=...) and address each run with a thread_id. Use InMemorySaver while developing, SqliteSaver for a single machine, and PostgresSaver in production. This tutorial builds a two-node graph, kills it on purpose, and resumes it from disk without re-running (or re-paying for) the completed Claude call.

What you need

You need Python 3.10+, an Anthropic API key in ANTHROPIC_API_KEY, and three packages. The checkpointer classes live in their own installable packages, which trips people up.

Python Install the graph, the SQLite checkpointer, and the Claude binding:

bash
pip install langgraph langgraph-checkpoint-sqlite langchain-anthropic

InMemorySaver ships inside langgraph itself. SqliteSaver and PostgresSaver are separate installs: langgraph-checkpoint-sqlite and langgraph-checkpoint-postgres. If you have never assembled a graph before, build one first in our LangGraph graph-agent tutorial, then come back to make it durable. Everything below follows the LangGraph persistence docs (2026) and the langgraph repo.

The graph we are going to make durable

Two nodes. generate calls Claude once, the expensive step. publish sends the result somewhere that can fail: a rate limit, a deploy hiccup, a flaky webhook. We will make publish fail the first time on purpose.

python
# app.py
import os, sqlite3
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-5", max_tokens=300)

class State(TypedDict):
    topic: str
    draft: str
    published: bool

def generate(state: State) -> dict:
    print("generate: calling Claude (this one costs money)")
    msg = llm.invoke(f"Write a one-line changelog entry for: {state['topic']}")
    return {"draft": msg.content.strip()}

def publish(state: State) -> dict:
    # Simulate a downstream failure the FIRST time only.
    if not os.path.exists("publish.ok"):
        open("publish.ok", "w").close()
        raise RuntimeError("publish API timed out")
    print("publish: shipped ->", state["draft"])
    return {"published": True}

builder = StateGraph(State)
builder.add_node("generate", generate)
builder.add_node("publish", publish)
builder.add_edge(START, "generate")
builder.add_edge("generate", "publish")
builder.add_edge("publish", END)

Nothing here is durable yet. If publish throws, the Claude call in generate is gone, and re-running pays for it again.

Add a checkpointer in one line

Attach a checkpointer at compile time. That is the whole change.

python
conn = sqlite3.connect("checkpoints.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)
graph = builder.compile(checkpointer=checkpointer)

Now LangGraph writes a checkpoint after every super-step into checkpoints.db. Because it is SQLite on disk, those checkpoints outlive the process. That is the part InMemorySaver cannot do.

thread_id: the key that makes resume work

A checkpointer stores state per thread_id. The same id means "continue this run"; a new id means "start fresh." You pass it in the config on every call.

python
config = {"configurable": {"thread_id": "changelog-42"}}

Reuse changelog-42 and you resume that exact run. This same mechanism is how a checkpointer gives a chat agent per-conversation memory, and how human-in-the-loop pauses resume after a human replies.

Crash it, then resume without re-paying for Claude

First run. generate calls Claude and checkpoints the draft, then publish raises and the script dies.

python
if __name__ == "__main__":
    graph.invoke(
        {"topic": "add dark mode toggle", "published": False},
        config,
    )
text
generate: calling Claude (this one costs money)
RuntimeError: publish API timed out

The process is gone. But checkpoints.db holds the state right after generate. Run the SAME script again, this time resuming instead of starting over:

python
    # second run: pass None to continue the existing thread from its last checkpoint
    graph.invoke(None, config)
text
publish: shipped -> Added a dark mode toggle to settings.

Read that output carefully: there is no second generate: calling Claude line. LangGraph resumed at the failed publish step, reused the saved draft, and never re-ran or re-billed the Claude call. On a real pipeline where step one is a twenty-cent model call or a ten-minute job, that is the difference between a cheap retry and redoing everything.

Passing None as the input is the resume signal. It means "no new input, continue thread changelog-42 from where it stopped."

Inspect every checkpoint

The saved state is not a black box. get_state shows the current snapshot and what is still pending; get_state_history walks every checkpoint newest-first.

python
snap = graph.get_state(config)
print(snap.values)     # {'topic': ..., 'draft': ..., 'published': True}
print(snap.next)       # () when done; ('publish',) right after the crash

for st in graph.get_state_history(config):
    print(st.config["configurable"]["checkpoint_id"], "->", st.next)

snap.next is the proof. Right after the crash it reads ('publish',), which is why the resume re-ran publish and left generate alone. Each entry in the history is a StateSnapshot you can also resume or fork from for time travel.

InMemory vs SQLite vs Postgres: pick by where you run

Same interface, three backends. Swap the one line; the graph does not change.

Scroll to see more

CheckpointerImportInstallUse it for
InMemorySaverfrom langgraph.checkpoint.memory import InMemorySaverships with langgraphdev, tests, single process. Lost on exit.
SqliteSaverfrom langgraph.checkpoint.sqlite import SqliteSaverlanggraph-checkpoint-sqliteone machine, survives restarts.
PostgresSaverfrom langgraph.checkpoint.postgres import PostgresSaverlanggraph-checkpoint-postgresproduction, many workers.

For Postgres, create the tables once with .setup():

python
from langgraph.checkpoint.postgres import PostgresSaver

checkpointer = PostgresSaver.from_conn_string("postgresql://localhost/agents")
checkpointer.setup()   # one time: creates the checkpoint tables
graph = builder.compile(checkpointer=checkpointer)

InMemorySaver looks fine in a notebook, but it cannot do the crash-and-resume above, because the state dies with the process. If you want durability, you need SQLite or Postgres. Nothing about the checkpointer is Claude-specific either. Swap ChatAnthropic for any LangChain chat model and the persistence works the same.

Verify your setup (break it on purpose)

Do not trust a checkpointer you have not watched recover. Three checks:

  1. Delete publish.ok, run app.py once, watch publish raise. Confirm checkpoints.db now exists on disk.
  2. Run app.py again with the same thread_id. Confirm it prints publish: shipped and NOT a second generate line. That is resume working.
  3. Change thread_id to changelog-43 and run again. It re-runs generate from scratch, because a new thread is a new run. That is proof the id, not the file, scopes the state.

If step two re-calls Claude, you either changed the thread_id or forgot to pass None on the resume call.

Limitations and open questions

  • A checkpoint stores your graph state, not external side effects. If publish already sent an email before it failed, resuming will send it again. Make failure-prone nodes idempotent, or gate them on state you persist.
  • SqliteSaver is single-machine. The moment you run two workers, move to PostgresSaver; concurrent writers against one SQLite file will bite you.
  • Checkpoints accumulate. A long thread keeps every super-step. For high-volume production, plan retention and cleanup rather than letting the table grow forever.
  • State is stored serialized. Keep large blobs like files or images in a store or object storage and keep only references in graph state.

Related reading: building a LangGraph graph agent and human-in-the-loop with LangGraph.

R

Written by

Ren Okabe

Ren Okabe builds and breaks agent systems, then writes down the runnable version. Principal-engineer voice, code first.

Frequently asked questions

What is a checkpointer in LangGraph?

A checkpointer is a persistence layer that saves your graph's state as a checkpoint after every super-step. It lets an agent pause, survive a crash, and resume from the last saved point instead of restarting. You attach one with builder.compile(checkpointer=...) and address each run with a thread_id.

What is the difference between a checkpointer and a store in LangGraph?

As of 2026, a checkpointer persists a single thread's graph state for short-term, thread-scoped memory: conversation continuity, human-in-the-loop, time travel, and fault tolerance. A store persists application-defined data outside the graph state and across threads, for long-term memory. Most agents use a checkpointer first and add a store only when they need cross-thread memory.

How do I resume a LangGraph run after a crash?

Use a durable checkpointer such as SqliteSaver or PostgresSaver, then re-invoke the graph with the same thread_id and None as the input: graph.invoke(None, config). LangGraph loads the last checkpoint and continues from the step that failed, without re-running the steps that already completed.

Does InMemorySaver survive a process restart?

No. InMemorySaver keeps checkpoints in memory only, so they are lost when the process exits. It is ideal for development and tests, but it cannot resume after a crash-and-restart. For that you need SqliteSaver on a single machine or PostgresSaver in production.

Do I need to call setup() on a LangGraph checkpointer?

Call .setup() once for the database-backed savers to create their tables. It is required for PostgresSaver. InMemorySaver needs no setup, and SqliteSaver creates its tables on first use, so a .setup() call is not needed for the in-memory or SQLite paths in the basic flow shown here (2026).