LangGraph Persistence and Checkpointers: Add Memory to Your Graph

In Chapter 9, you kept memory by passing the full history back into every invoke() yourself. That works, but it is manual and disappears when the program stops. Persistence lets LangGraph save and restore state for you.

The idea: checkpoints

A checkpointer saves a snapshot of the state (a checkpoint) as the graph runs. Next time you run the graph on the same conversation, LangGraph loads the last saved state and continues from there.

Two things are needed:

  1. A checkpointer, passed to compile()
  2. A thread ID, passed when you run the graph, which says which conversation to load

Example 1: See persistence with no LLM

import operator
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver

class State(TypedDict):
    log: Annotated[list[str], operator.add]

def add_visit(state: State):
    return {"log": ["visit"]}

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

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

config = {"configurable": {"thread_id": "t1"}}

print(graph.invoke({"log": ["start"]}, config))
print(graph.invoke({"log": ["again"]}, config))

Output:

{'log': ['start', 'visit']}
{'log': ['start', 'visit', 'again', 'visit']}

How it works:

  • The first run starts from an empty state, adds “start”, and the node adds “visit”
  • LangGraph saves the result under thread t1
  • The second run loads that saved state, merges the new input using the reducer (Chapter 5), and continues
  • Nothing from the first run was passed in by you

Threads keep conversations separate

A different thread ID means a different, empty state:

other = {"configurable": {"thread_id": "t2"}}
print(graph.invoke({"log": ["hello"]}, other))

Output:

{'log': ['hello', 'visit']}

Thread t1 still has its own history. Chapter 14 covers threads in depth.

Example 2: A chatbot that remembers

Now the Chapter 9 chatbot, with persistence instead of a manual history list:

from dotenv import load_dotenv
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import InMemorySaver

load_dotenv()
llm = ChatAnthropic(model="claude-sonnet-5-5")

def chatbot(state: MessagesState):
    system = SystemMessage("You are a friendly tutor. Keep answers under 50 words.")
    reply = llm.invoke([system] + state["messages"])
    return {"messages": [reply]}

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

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

config = {"configurable": {"thread_id": "student-1"}}

while True:
    text = input("You: ")
    if text.lower() in ("quit", "exit"):
        break
    result = graph.invoke({"messages": [("human", text)]}, config)
    print("Bot:", result["messages"][-1].content)

Sample conversation:

You: My name is Asha and I'm learning Python.
Bot: Nice to meet you, Asha! Python is a great choice. Where would you like to start?
You: What's my name?
Bot: Your name is Asha!

Compare this with Chapter 9: the loop no longer keeps a history list. You send only the new message, and the checkpointer supplies the rest.

Inspecting saved state

get_state() shows what is stored for a thread:

snapshot = graph.get_state(config)

print(snapshot.values["messages"][-1].content)   # latest message
print(snapshot.next)                              # what would run next

When a run has finished, snapshot.next is an empty tuple (). Later, in Chapter 15, you will see it list a waiting node when a graph is paused for human approval.

Looking back in time

Every step creates a checkpoint, and you can list them:

for snapshot in graph.get_state_history(config):
    print(snapshot.next, len(snapshot.values["messages"]))

The history comes newest first. Each entry shows the state at that moment, which is useful for debugging (“what did the state look like before the model answered?”). LangGraph can also resume from an earlier checkpoint, an advanced technique sometimes called time travel.

Saving to disk with SQLite

InMemorySaver keeps everything in RAM, so it is lost when your program stops. For memory that survives restarts, use a database checkpointer. SQLite is the easiest:

pip install -U langgraph-checkpoint-sqlite
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver

conn = sqlite3.connect("chat_memory.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)

graph = builder.compile(checkpointer=checkpointer)

Everything else stays the same. Run the chatbot, tell it your name, stop the program, run it again with the same thread_id, and ask “What’s my name?”. It still knows, because the conversation is stored in the file chat_memory.db.

Choosing a checkpointer

  • InMemorySaver: does not survive a restart. Best for learning, tests, and demos.
  • SqliteSaver: survives a restart (local file). Best for small apps and prototypes.
  • Postgres checkpointer: survives a restart (server database). Best for production apps with many users.

The graph code doesn’t change. You only swap the checkpointer passed to compile(). Chapter 23 covers deployment, where LangGraph’s hosting platform can manage persistence for you.

Privacy and safety notes

  • A checkpoint stores the full conversation, so treat the database like user data
  • Don’t let users guess each other’s thread IDs. In a real app, build the ID from a logged-in user’s identity, and check permissions on the server
  • Avoid saving secrets (passwords, API keys) in state

Common mistakes

  • Forgetting the thread_id: LangGraph raises an error saying the checkpointer needs a thread_id
  • Passing the checkpointer to invoke(): it belongs in compile()
  • Resending the full history: with a checkpointer, send only the new message
  • Using InMemorySaver and expecting it to survive a restart: switch to SQLite or Postgres
  • Reusing one thread ID for every user: everyone would share one conversation
  • Wrapping config wrongly: it must be {“configurable”: {“thread_id”: “…”}}

Summary

  • A checkpointer saves state snapshots, passed to compile(checkpointer=…)
  • A thread_id in the config chooses which conversation to load
  • With persistence, you send only new messages, and LangGraph restores the rest
  • get_state() and get_state_history() let you inspect saved state
  • InMemorySaver is for learning, while SQLite or Postgres keeps data across restarts

Practice

  1. Run Example 1 with thread t1, then a third time, and confirm the log keeps growing
  2. Run the chatbot with two thread IDs (asha and ravi), give each a different name, and check that they don’t mix
  3. Switch to SqliteSaver, stop the program, restart it, and confirm the bot still remembers your name

If you liked the tutorial, spread the word and share the link and our website, Studyopedia, with others.


For Videos, Join Our YouTube Channel: Join Now


Read More:

LangGraph Tools and Tool Calling
LangGraph Threads: Handle Multiple Conversations and Users
Studyopedia Editorial Staff
contact@studyopedia.com

We work to create programming tutorials for all.

No Comments

Post A Comment