Checkpointers and threads

LangGraph · Practical · 12 min

Persistence powers memory, human-in-the-loop, time travel and crash recovery; nearly every real app needs it.

A checkpointer saves a snapshot of the graph's state (a checkpoint) after every step. Attach one when compiling:

graph = builder.compile(checkpointer=InMemorySaver())

Checkpoints are grouped into threads. A thread is one conversation or one job, named by a thread_id you choose and pass in the config:

config = {"configurable": {"thread_id": "user-42-chat-1"}}
graph.invoke({"messages": [...]}, config)

Call again with the same thread_id and the graph starts from the saved state, so a chatbot remembers earlier messages; you send only the new message. A different thread_id starts fresh. This is short-term memory: scoped to one thread.

Inspect a thread with graph.get_state(config). It returns a snapshot with .values (the state), .next (nodes that would run next; empty when finished) and more.

Which checkpointer:

  • InMemorySaver for tests and experiments (lost when the process ends),
  • SqliteSaver (package langgraph-checkpoint-sqlite) for local apps,
  • PostgresSaver (langgraph-checkpoint-postgres) and others such as MongoDB or Redis for production. Call .setup() once to create tables.

When you deploy with LangSmith Deployment, persistence is provided for you and you don't pass a checkpointer.

State is serialized when saved, so keep it to data types the serializer understands (plain data, LangChain messages, Pydantic models).

Example

from langchain_core.messages import AIMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END, MessagesState


def bot(state: MessagesState) -> dict:
    return {"messages": [AIMessage(f"I have seen {len(state['messages'])} messages")]}


b = StateGraph(MessagesState)
b.add_node(bot)
b.add_edge(START, "bot")
b.add_edge("bot", END)
graph = b.compile(checkpointer=InMemorySaver())

chat1 = {"configurable": {"thread_id": "chat-1"}}
graph.invoke({"messages": [{"role": "user", "content": "hi"}]}, chat1)
out = graph.invoke({"messages": [{"role": "user", "content": "again"}]}, chat1)
print(out["messages"][-1].content)                 # I have seen 3 messages

chat2 = {"configurable": {"thread_id": "chat-2"}}
print(graph.invoke({"messages": [{"role": "user", "content": "hi"}]}, chat2)["messages"][-1].content)
# I have seen 1 messages
print(graph.get_state(chat1).next)                # () -> finished

Remember

  • compile(checkpointer=...) saves state after every step
  • thread_id in config['configurable'] selects the conversation
  • Same thread continues; new thread starts fresh
  • InMemorySaver for tests; Postgres/SQLite/etc. for real apps

Watch out for

  • Forgetting thread_id when a checkpointer is set; LangGraph raises an error asking for it.

Check yourself

  1. A user opens a second, separate chat. How do you keep its history apart?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.langchain.com/oss/python/langgraph/persistence