LangGraph MemorySaver & SqliteSaver Setup Errors
MemorySaver is perfect for tests and loses everything on restart. SqliteSaver persists but needs schema initialization. Here are the three setup errors developers hit in their first hour with checkpointing — and the exact fixes.
By Ahmed R. · Last updated Aug 14, 2026 · LangGraph · Page #127
thread_id in the runnable config on every invoke, (2) explicit schema setup for the persistent variants (SqliteSaver, PostgresSaver), and (3) a matching sync/async saver for your app (SqliteSaver for sync code, AsyncSqliteSaver for async). MemorySaver is in-process only — it evaporates on restart. If you need persistence in prod, use SQLite for single-instance or Postgres for multi-instance.Real error messages you'll see
ValueError: Missing configuration key 'thread_id'. Every invocation of a checkpointed graph requires config={"configurable": {"thread_id": "..."}}.
at Pregel.astream() / Pregel.invoke()
# You compiled the graph with a checkpointer but called app.invoke(state) without config.
sqlite3.OperationalError: no such table: checkpoints
at SqliteSaver.get_tuple() / put()
# The SQLite database exists but the checkpointer schema was never initialized.
# Fix: call SqliteSaver.setup() once, or use the from_conn_string classmethod which auto-runs setup on first use in recent versions.
RuntimeError: SqliteSaver is a synchronous checkpointer; use AsyncSqliteSaver inside an async event loop.
at Pregel.astream()
# You imported SqliteSaver but the graph is invoked from an async function (FastAPI, aiohttp). Use AsyncSqliteSaver instead.
Choose the right checkpointer
| Checkpointer | When to use | Persistence |
|---|---|---|
MemorySaver | Tests, notebooks, ephemeral demos | None — cleared on restart |
SqliteSaver | Single-instance apps, dev, small prod | File on disk; single-writer |
AsyncSqliteSaver | Async apps (FastAPI, aiohttp) on single instance | File on disk; single-writer |
PostgresSaver | Multi-instance prod, high throughput | Postgres server; multi-writer |
AsyncPostgresSaver | Async multi-instance prod | Postgres server; multi-writer |
| Custom (Redis, Mongo, etc.) | When Postgres isn't an option | Implement BaseCheckpointSaver |
Root causes (ranked by frequency)
Based on LangGraph developer reports; percentages sum to 100%.
- 28%Missing
thread_idin config. Compiled with a checkpointer but calledapp.invoke(state)withoutconfig={"configurable": {"thread_id": "x"}}. LangGraph raises immediately. - 19%SqliteSaver schema not initialized. Used the constructor directly (
SqliteSaver(conn)) without calling.setup(). The DB file exists but tables don't. - 15%Sync/async saver mismatch. Imported
SqliteSaverbut the graph runs insideasync def. Or vice versa. RaisesRuntimeError. - 12%Expecting persistence from
MemorySaver. App restarts; all checkpoints are gone.MemorySaveris a dict; there is no disk. - 9%SQLite locked by another process. Multiple worker processes hit the same SQLite file.
sqlite3.OperationalError: database is locked. Move to Postgres for multi-writer. - 7%Checkpointer added AFTER compile.
app = graph.compile()thenapp.checkpointer = memory. Ignored; checkpointer must be passed tocompile(checkpointer=...). - 5%Wrong connection URI format.
SqliteSaver.from_conn_string("data.db")works;SqliteSaver.from_conn_string("./data.db")may fail on some platforms because of path resolution. Use absolute paths in prod. - 5%Async saver context manager not entered.
AsyncSqliteSaver.from_conn_string(...)returns an async context manager — must be used withasync with, not directly assigned.
How to fix it
Pass thread_id in the config on every invocation
The one-line fix for "Missing configuration key thread_id".
A checkpointed graph groups related invocations by thread_id. The thread_id is your conversation/session identifier — every call with the same thread_id reads and writes to the same checkpoint. Skipping it isn't optional; LangGraph refuses to run without knowing which thread the invocation belongs to.
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END, MessagesState
def responder(state):
reply = llm.invoke(state["messages"])
return {"messages": [reply]}
graph = StateGraph(MessagesState)
graph.add_node("responder", responder)
graph.add_edge(START, "responder")
graph.add_edge("responder", END)
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer) # <-- attached at compile
# ✅ Always pass thread_id in config
thread_id = str(uuid.uuid4()) # or a user_id, session_id, etc.
config = {"configurable": {"thread_id": thread_id}}
result = app.invoke(
{"messages": [HumanMessage(content="Hello")]},
config, # <-- required
)
# Same thread_id continues the conversation
result = app.invoke(
{"messages": [HumanMessage(content="What did I just say?")]},
config, # ← same config, reads prior state
)
# ❌ WRONG — missing config, raises ValueError
# result = app.invoke({"messages": [...]})
# ✅ FastAPI pattern — thread_id from URL param
from fastapi import FastAPI
api = FastAPI()
@api.post("/chat/{session_id}")
async def chat(session_id: str, message: str):
config = {"configurable": {"thread_id": session_id}}
return app.invoke(
{"messages": [HumanMessage(content=message)]},
config,
)
thread_id can be any string, but stable identifiers matter: use the session cookie, the user ID + chat ID, or a UUID stored in a cookie. Never generate a fresh UUID inside your request handler — every call would start a new conversation.Initialize SqliteSaver with the from_conn_string classmethod
Fixes "no such table" errors and handles schema setup automatically.
SqliteSaver.from_conn_string(":memory:") or SqliteSaver.from_conn_string("data.db") is a context manager that opens the connection AND runs schema setup on first use. This is the safest way to create a checkpointer in modern LangGraph — you don't have to remember to call setup().
from pathlib import Path
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END, MessagesState
# ✅ from_conn_string — auto-runs setup(), returns a context manager
DB_PATH = Path("/var/data/langgraph_checkpoints.db").as_posix()
with SqliteSaver.from_conn_string(DB_PATH) as checkpointer:
graph = StateGraph(MessagesState)
graph.add_node("responder", responder)
graph.add_edge(START, "responder")
graph.add_edge("responder", END)
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42"}}
result = app.invoke({"messages": [HumanMessage(content="Hi")]}, config)
# ✅ For long-lived apps (FastAPI), open once at startup and keep the conn
import sqlite3
from contextlib import asynccontextmanager
from fastapi import FastAPI
CHECKPOINT_DB = "/var/data/langgraph.db"
@asynccontextmanager
async def lifespan(app: FastAPI):
conn = sqlite3.connect(CHECKPOINT_DB, check_same_thread=False)
checkpointer = SqliteSaver(conn)
checkpointer.setup() # explicit setup for the raw constructor
app.state.graph = build_graph(checkpointer)
yield
conn.close()
api = FastAPI(lifespan=lifespan)
# ❌ WRONG — using SqliteSaver constructor directly without setup()
# conn = sqlite3.connect("data.db")
# checkpointer = SqliteSaver(conn) # no setup() call
# app = graph.compile(checkpointer=checkpointer)
# # First invoke raises sqlite3.OperationalError: no such table
# ✅ In-memory SQLite — perfect for tests
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
app = build_graph(checkpointer)
# ... run tests ...
# DB is gone when the `with` block exits — no cleanup needed
with SqliteSaver.from_conn_string(...) as) is preferred for scripts, one-off jobs, and tests. For long-running processes, hold the connection open at startup and pass the checkpointer to a graph factory — reopening on every request is slow.Use AsyncSqliteSaver inside async apps
Fixes RuntimeError: SqliteSaver is a synchronous checkpointer.
Sync savers block the event loop and LangGraph refuses to use them inside async def. The async variants (AsyncSqliteSaver, AsyncPostgresSaver) are drop-in replacements. Same API, different import, safe under async.
# ✅ Async SQLite — for FastAPI, aiohttp, or any asyncio app
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
from langgraph.graph import StateGraph, START, END, MessagesState
async def responder(state):
reply = await llm.ainvoke(state["messages"])
return {"messages": [reply]}
# from_conn_string is an ASYNC context manager here
async def run_chat():
async with AsyncSqliteSaver.from_conn_string("/var/data/chat.db") as checkpointer:
graph = StateGraph(MessagesState)
graph.add_node("responder", responder)
graph.add_edge(START, "responder")
graph.add_edge("responder", END)
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42"}}
result = await app.ainvoke(
{"messages": [HumanMessage(content="Hello")]},
config,
)
return result
# ✅ FastAPI lifespan pattern for long-lived app
from contextlib import asynccontextmanager
import aiosqlite
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
conn = await aiosqlite.connect("/var/data/langgraph.db")
checkpointer = AsyncSqliteSaver(conn)
await checkpointer.setup() # await, not call
app.state.graph = build_graph(checkpointer)
yield
await conn.close()
api = FastAPI(lifespan=lifespan)
# ❌ Common mistake — sync saver in async handler
# from langgraph.checkpoint.sqlite import SqliteSaver
# @api.post("/chat")
# async def chat(msg: str):
# conn = sqlite3.connect("data.db")
# saver = SqliteSaver(conn); saver.setup()
# app = build_graph(saver)
# return await app.ainvoke(...) # RuntimeError: sync checkpointer
SqliteSaver; async app → AsyncSqliteSaver. The two are not interchangeable. If you're using app.invoke use sync; if app.ainvoke or app.astream, use async.Prevention checklist
- Every checkpointed graph invocation needs
config={"configurable": {"thread_id": "..."}}. Make this a wrapper function in your codebase. - Use
SqliteSaver.from_conn_string(...)instead of the raw constructor — it auto-runs schema setup. - Match the saver to your code style:
SqliteSaverfor sync apps,AsyncSqliteSaverfor async. - Never use
MemorySaverin production — it evaporates on restart. - SQLite is single-writer. For multiple worker processes or replicas, migrate to
PostgresSaver. - Compile the graph with the checkpointer, not by attaching it after:
graph.compile(checkpointer=saver). - Use absolute file paths for SQLite in production; relative paths are resolved against wherever the process was started.
Frequently asked questions
Only if you want (a) resumable state across invocations (chat threads, agents that pause for input), (b) time-travel debugging, or (c) human-in-the-loop interrupts. Stateless graphs — a document classifier that runs once per input — work fine without one. If you're building a chat bot, you need a checkpointer.
Yes — just change the saver you pass to compile. State schema is identical across savers. But existing in-memory checkpoints don't migrate; they live only as long as the process. If you're moving to production, plan to start fresh (or write a one-off export script that reads from the old saver and writes to the new one before shutdown).
SQLite is a single-writer database. With one worker process it's fine; with multiple gunicorn/uvicorn workers each opening the same file, they'll contend on the write lock and one will fail. SQLite has some workarounds (WAL mode, longer busy timeout) but for multi-process production, use PostgresSaver. It's a straight swap — same API, no code change beyond the saver import and connection string.
Open the file with any SQLite client: sqlite3 langgraph.db "SELECT thread_id, checkpoint_id, ts FROM checkpoints ORDER BY ts DESC LIMIT 20". LangGraph also provides checkpointer.list(config) to enumerate checkpoints for a thread programmatically. In dev, LangSmith's trace view visualizes checkpoints inline with the run graph.
Yes. Every state key — including messages — is serialized into the checkpoint. This is how conversations resume. On very long threads the checkpoints can grow large; if that's a concern, add a prune node that emits RemoveMessage tombstones once history exceeds a cap. Alternatively, offload long history to external storage and keep only a reference (message IDs or a summary) in state.