LangChainLangChain 1.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your path

Saving threads with a SqliteSaver checkpointer

SqliteSaver is a checkpointer that keeps every thread in a file. A conversation is still there after the program exits and starts again.

Last updated: 27 Sep, 2026 · LangChain 1.4

SqliteSaver keeps the same threads in a file. It comes from langgraph-checkpoint-sqlite and takes an open connection.

The SqliteSaver connection

python
import sqlite3

from langgraph.checkpoint.sqlite import SqliteSaver

saver = SqliteSaver(sqlite3.connect("desk.db", check_same_thread=False))

The imports for the sqlite saver

check_same_thread=False lets the agent read the connection from whichever thread it runs on. In desk.py the saver goes where InMemorySaver() was.

In desk.py the imports gain sqlite3 and the sqlite saver, in place of the in-memory one.

python
import sqlite3

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, ModelCallLimitMiddleware, PIIMiddleware
from langgraph.checkpoint.sqlite import SqliteSaver
from desk_model import DeskModel
from guard import no_passwords
from search import search_policies
from tools import Customer, lookup_order, refund_order

The file-backed saver argument

The last argument to create_agent becomes the file-backed saver, and the rest of the file stays as it is.

python
    checkpointer=SqliteSaver(sqlite3.connect("desk.db", check_same_thread=False)),

The desk.py it changes

The whole file, with the two changed lines in place.

python
import sqlite3

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, ModelCallLimitMiddleware, PIIMiddleware
from langgraph.checkpoint.sqlite import SqliteSaver
from desk_model import DeskModel
from guard import no_passwords
from search import search_policies
from tools import Customer, lookup_order, refund_order

agent = create_agent(
    DeskModel(),
    tools=[lookup_order, refund_order, search_policies],
    context_schema=Customer,
    middleware=[
        no_passwords,
        PIIMiddleware("credit_card", strategy="mask"),
        ModelCallLimitMiddleware(run_limit=6),
        HumanInTheLoopMiddleware(interrupt_on={"refund_order": True}),
    ],
    checkpointer=SqliteSaver(sqlite3.connect("desk.db", check_same_thread=False)),
)

An import, a connection, and the argument that was already there. Everything the desk does with threads was written against the checkpointer, so the rest of the file is untouched.

Ask once, then run it again

The program below asks one question and counts what the thread holds. Running it twice is two separate processes, which is what a restart is.

python
from chat import say
from desk import agent

thread = {"configurable": {"thread_id": "ravi-1"}}
say("ravi", "Where is A17?", "ravi-1")
print(len(agent.get_state(thread).values["messages"]), "messages saved")
Example
python threads.py
python threads.py

Four messages after the first run, eight after the second: the second process found Ravi's thread already there and carried on. With InMemorySaver both runs would have printed four.

Why the second run found the thread

  • Four then eight. The first run saved four messages; the second process found Ravi's thread already on disk and continued, reaching eight.
  • A restart is two processes. Running the file twice is what a restart is, and the file is what carries the thread across.
  • InMemorySaver would print four both times. It holds threads in memory only, so each run would start empty.

The tests, against the real pieces

The five tests from the project were written against the desk, not against the stand-ins. The store is Chroma now and the saver writes to a file, and nothing in the test file changes.

Example
pytest -q -p no:warnings test_desk.py

That is what those tests were for. Two pieces of the desk were replaced by packages it had never run against, and the owner check, the policy answer, the refusal and the guardrail all still hold.

The finished desk, and what each piece becomes
DeskModelyou wrote itBaseChatModelthe interfaceinit_chat_modelyou install itWordEmbeddingscounts wordsEmbeddingsthe interfaceOpenAIEmbeddingslangchain-openaiInMemoryVectorStorea list in memoryVectorStorethe interfaceChromalangchain-chroma, on diskInMemorySavera dict in memoryBaseCheckpointSaverthe interfaceSqliteSaverdesk.db, or Postgres
Hover or tap a piece to see what it is and which lesson built it.
Swap by swap

Pick one to watch it run, step by step.

The desk as it now stands, and what each piece it was built on becomes. The middle column is the only reason the swaps are one line each: everything around them was written against the interface, never against your class.

When threads must survive a restart

  • Any desk that must remember a customer between visits, or across a deploy.
  • A service on more than one machine, where PostgresSaver takes a connection string instead of a file.
Watch out. A file-backed saver needs check_same_thread=False on the connection, or the agent raises when it reads the thread from a different thread than the one that opened the file.

The file is the only difference. For a service with more than one machine talking to it, PostgresSaver from langgraph-checkpoint-postgres takes a connection string instead, and the desk around it stays as it is.

Try it yourself
  • Delete desk.db and run threads.py twice again.
  • Ask as a second customer on another thread id and look at the file's size.
  • Read agent.get_state_history(thread) after three questions.

Every expert started right here.