LangGraph: fixing 'Checkpointer requires one or more of the following configurable keys: thread_id, checkpoint_ns, checkpoint_id'
A graph compiled with a checkpointer needs a thread_id in config['configurable'] on every call. Here's where it goes, why context= doesn't count, why LangServe drops it, and why None or an empty string is worse than the error.
Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this article with AI assistance and checked it against the GitHub issues, the LangGraph persistence docs, the LangServe code and README and the MemorySync docs listed under Sources on Oct 30, 2026. Code status: I ran the repro, every row of the three tables, thread_config, the LangServe fix and the Step 5 agent check offline against langgraph 0.4.10, 0.5.1 and 1.2.18 (langgraph-checkpoint 2.0.26, 2.1.0 and 4.3.1) with InMemorySaver, no model or LangChain's scripted fake chat model, and LangServe called through FastAPI's in-process TestClient (no provider calls, no network). The Step 5 agent check ran on langgraph 1.2.18 with langchain 1.4.7 only. I didn't run Postgres, SQLite or Redis checkpointers, the AG-UI or CopilotKit code, or the MemorySync snippet.
You compiled a LangGraph graph (or a create_agent agent) with a checkpointer, called it, and got:
ValueError: Checkpointer requires one or more of the following 'configurable' keys: thread_id, checkpoint_ns, checkpoint_id
That is the error in LangChain issue #34363, where it came from a call that passed runtime context= (with a session_id in it) but no config. In LangGraph issue #5385 the same check printed an empty list, ... 'configurable' keys: [], after an upgrade from langgraph-checkpoint 2.0.26 to 2.1.0, with the graph called through LangServe's RemoteRunnable and a thread_id that the client did send.
A checkpointer saves state per thread, so it needs to know which thread. As the LangGraph persistence docs put it, the access pattern for a checkpointer is "Pass a thread_id in graph config". This page shows where that goes, the cases where you think you passed it and didn't, and one case where you pass the wrong thing and get no error at all.
MemorySync doesn't fix this bug. The error is about the checkpointer's thread config, and you need a thread_id with or without MemorySync. LangGraph behaviour here comes from the issues, the persistence docs, the LangServe source and my offline runs.
Skip the memory plumbing around this setup. Once each call carries its
thread_id, the MemorySync LangGraph guide shows how to add long-term memory per user across threads with one middleware forcreate_agent. It doesn't replace the checkpointer or supply athread_id, so you still need the fix below. When you're ready, see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
Status (Nov 9, 2026)
- #34363 (opened Dec 15, 2025, 2 comments) is closed. The maintainer reply was that the
contextparameters exist on recent versions (reply); the follow-up with the checkpointer error got no answer in the thread. - #5385 (opened Jul 7, 2025, 5 comments) was closed for lack of a reproducible example. The last comment, from another user, points at the cause: in 2.1.0 "runnable.checkpointer.config_specs is empty" (comment). I reproduce both the
[]message and the LangServe failure below. - The error is still raised on langgraph 1.2.18 with langgraph-checkpoint 4.3.1 (PyPI), which I ran offline. As of Nov 9, 2026 the latest langgraph on PyPI is 1.2.20 (checkpoint still 4.3.1); I didn't re-run 1.2.20. It's a usage error, not a bug waiting for a fix.
Step 1: Reproduce it
This ran offline; there is no model in the graph:
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
class State(TypedDict):
n: int
builder = StateGraph(State)
builder.add_node("bump", lambda s: {"n": s["n"] + 1})
builder.add_edge(START, "bump")
builder.add_edge("bump", END)
graph = builder.compile(checkpointer=InMemorySaver())
graph.invoke({"n": 0}) # ValueError: Checkpointer requires one or more of the following 'configurable' keys: ...
The wording depends on the versions you have installed:
| langgraph | langgraph-checkpoint | Message after "the following 'configurable' keys:" |
|---|---|---|
| 0.4.10 | 2.0.26 | ['thread_id', 'checkpoint_ns', 'checkpoint_id'] |
| 0.4.10 | 2.1.0 | [] (the #5385 wording) |
| 0.5.1 | 2.1.0 | thread_id, checkpoint_ns, checkpoint_id |
| 1.2.18 | 4.3.1 | thread_id, checkpoint_ns, checkpoint_id |
All four mean the same thing. The [] comes from older langgraph printing the checkpointer's config_specs, which were empty from langgraph-checkpoint 2.1.0 on in my runs; newer langgraph prints a fixed list instead.
Step 2: Put thread_id in config, on every call
The fix is a config with configurable.thread_id, passed to every invoke, ainvoke, stream and astream on that graph. I ran each shape below on langgraph 1.2.18:
| Call | Result |
|---|---|
graph.invoke(input) |
ValueError: Checkpointer requires ... |
graph.invoke(input, {}) |
ValueError: Checkpointer requires ... |
graph.invoke(input, context=Ctx(user_id="123", session_id="abc")) (#34363 shape) |
ValueError: Checkpointer requires ... |
graph.invoke(input, {"configurable": {"user_id": "123"}}) |
KeyError: 'thread_id' (from InMemorySaver.get_tuple) |
graph.invoke(input, {"configurable": {"thread_id": "abc"}}) |
OK, saved under thread abc |
graph.invoke(input, {"thread_id": "abc"}) |
OK, saved under thread abc |
graph.invoke(input, {"configurable": {"thread_id": "abc"}}, context=Ctx(...)) |
OK |
graph.get_state({}) |
KeyError: 'configurable' |
Three things from that table:
context=is notconfig=. Runtime context is data your nodes and middleware read. The checkpointer never looks at it, even when your context class has a field namedsession_idorthread_id. Pass both if you use both.- The check only asks for a non-empty
configurable. Put anything else in it and you skip theValueError, then the saver fails on the missing key instead (KeyError: 'thread_id'withInMemorySaver). Same fix. - A top-level
thread_idworked. LangChain Core'sensure_configmoves unknown top-level config keys intoconfigurable, so{"thread_id": "abc"}was saved under threadabc. I'd still write theconfigurableform, which is what the docs show.
Step 3: Don't pass None or an empty string
This is the case to worry about, because it doesn't raise. In my runs {"configurable": {"thread_id": None}} and {"configurable": {"thread_id": ""}} both passed the check, on langgraph 0.5.1 and 1.2.18 with InMemorySaver. Two different users calling with thread_id=None landed in the same thread: Bob's second call returned Alice's message in its state (["alice: I'm vegetarian", 'bob: hi']). That usually happens when the ID comes from a request field or session lookup that came back empty.
Build the config in one place and refuse an empty ID:
def thread_config(thread_id: str | None) -> dict:
"""Build the config a checkpointed graph needs, and refuse a missing thread ID."""
if thread_id is None or not str(thread_id).strip():
raise ValueError("thread_id is required: pass the conversation's ID, not None or ''")
return {"configurable": {"thread_id": str(thread_id)}}
result = graph.invoke(inputs, thread_config(conversation_id))
With this, thread_config(None), thread_config("") and thread_config(" ") raised ValueError, and Alice's and Bob's threads stayed separate. Use an ID you store for the conversation (the persistence docs generate a UUID in their example), not the user ID, unless you really want one thread per user forever. I only tested None and "" with InMemorySaver; other savers may store or reject them differently.
Step 4: Behind LangServe, put thread_id back on the server
This is the #5385 case. LangServe validates the client's config against the graph's config schema, and that schema is built from config_specs. When those are empty, thread_id doesn't survive validation, so the graph runs with no thread and raises. I served a checkpointed graph with add_routes and posted {"input": {"n": 0}, "config": {"configurable": {"thread_id": "t1"}}} through FastAPI's TestClient:
| LangServe | langgraph | langgraph-checkpoint | POST /graph/invoke with a thread_id |
|---|---|---|---|
| 0.3.1 | 0.4.10 | 2.0.26 | 200 |
| 0.3.1 | 0.4.10 | 2.1.0 | 500, ... keys: [] |
| 0.3.1 | 0.5.1 | 2.1.0 | 500, ... keys: thread_id, checkpoint_ns, checkpoint_id |
| 0.3.3 | 1.2.18 | 4.3.1 | 500, ... keys: thread_id, checkpoint_ns, checkpoint_id |
So the client was right, and pinning langgraph-checkpoint to 2.0.26 only works on old langgraph. What worked on both 0.3.1 and 0.3.3 is reading thread_id from the raw request in a per_req_config_modifier, which LangServe applies after validation:
from fastapi import Request
from langserve import add_routes
async def keep_thread_id(config: dict, request: Request) -> dict:
"""Put configurable.thread_id from the request body back into the config."""
body = await request.json()
thread_id = (body.get("config") or {}).get("configurable", {}).get("thread_id")
if not thread_id:
raise ValueError("send config.configurable.thread_id with every request")
config.setdefault("configurable", {})["thread_id"] = str(thread_id)
return config
add_routes(app, graph, path="/graph", per_req_config_modifier=keep_thread_id)
With it, /graph/invoke and /graph/stream returned 200, calls with the same thread_id shared state, t1 and t2 were saved separately, and a request with no thread_id was refused (500 from the ValueError; return a 4xx from your own handler if you prefer). Check it against your own clients and auth before relying on it: anyone who can call the route can pick any thread_id, so map it to the user on the server if threads are private. The LangServe README now says: "We recommend using LangGraph Platform rather than LangServe for new projects."
Step 5: Check it with a scripted model
This check makes no provider calls. GenericFakeChatModel returns scripted replies, so it proves the thread is kept, not that a real model would answer this way. On langgraph 1.2.18 with langchain 1.4.7:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langgraph.checkpoint.memory import InMemorySaver
from thread_config import thread_config
@dataclass
class Context:
user_id: str
model = GenericFakeChatModel(messages=iter(["Hi Sam.", "You said your name is Sam."]))
agent = create_agent(model=model, tools=[], context_schema=Context, checkpointer=InMemorySaver())
try:
agent.invoke({"messages": [{"role": "user", "content": "I'm Sam."}]}, context=Context(user_id="123"))
except ValueError as e:
print("context only:", e)
config = thread_config("conv-abc")
agent.invoke({"messages": [{"role": "user", "content": "I'm Sam."}]}, config=config, context=Context(user_id="123"))
out = agent.invoke({"messages": [{"role": "user", "content": "What's my name?"}]}, config=config, context=Context(user_id="123"))
print([m.content for m in out["messages"]])
It printed:
context only: Checkpointer requires one or more of the following 'configurable' keys: thread_id, checkpoint_ns, checkpoint_id
["I'm Sam.", 'Hi Sam.', "What's my name?", 'You said your name is Sam.']
The second call's state holds the first call's messages, so the checkpointer is keeping the thread. Swap in your real model and your real conversation IDs before you change anything in production.
Skip the plumbing with MemorySync
Disclosure: I build MemorySync.
This doesn't change the fix above: a checkpointed graph still needs a thread_id on every call. The MemorySync LangGraph guide draws the same line. It lists thread state (checkpoints, resume, time travel) under "A checkpointer" and long-term memory "across threads, sessions and surfaces" under MemorySync. For create_agent it documents MemorySyncMemoryMiddleware(user_id=...), which injects recalled context before the model call, and says the injection shapes only what the model sees for that call: "checkpointed graph state is never mutated". Its persist node uses the graph's thread_id as the session by default.
So the user identity goes to MemorySync and the conversation ID goes to the checkpointer, and they stay separate. A new thread_id starts an empty thread. The guide's complete example turns on auto_persist=True (off by default, per the guide) and shows a second thread getting what the first one learned for that user. Limits from the guide: the LangGraph integration requires langgraph 1.2+ in Python, so it isn't an option on the 0.x versions in the LangServe table, and it lists langchain-memorysync[langgraph] 1.2.0. I haven't run it with the steps on this page.
From the guide:
Unverified (not run):
pip install "langchain-memorysync[langgraph]"
from langchain.agents import create_agent
from langchain_memorysync.langgraph import MemorySyncMemoryMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="openai:your-model",
tools=tools,
middleware=[MemorySyncMemoryMiddleware(user_id="customer-7")],
checkpointer=InMemorySaver(),
)
agent.invoke({"messages": [{"role": "user", "content": "Hi"}]}, config=thread_config("conv-abc"))
Snippet adapted from the MemorySync LangGraph guide; I added the model string, the checkpointer and thread_config from Step 3, and haven't run it for this page. Set MEMORYSYNC_API_KEY in the environment, as the guide says. For when to use a checkpointer and when to use a store, see my earlier tutorial, LangGraph memory explained: checkpointer vs store.
Try it: MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, see MemorySync, create an API key in the dashboard and drop the snippet into your agent.
Troubleshooting
- The error says
keys: []. You're on an older langgraph with langgraph-checkpoint 2.1.0 or later. The fix is the same: passconfigurable.thread_id. If you're behind LangServe, see Step 4. KeyError: 'thread_id'instead.configurablewas there but had nothread_id(for example onlyuser_id). Add it.- It works the first time and fails on regenerate or resume in a UI. The UI layer made a call without your config. That is ag-ui #683 and CopilotKit #2719, both closed. In ag-ui-langgraph 0.0.35 and 0.0.47 the regenerate path passes a config to the stream call (I read the code; I didn't run it), so upgrade before you patch it yourself.
- No error, but users see each other's messages. Check for
thread_id=Noneor""(Step 3). - No error, but the conversation is gone after a restart.
InMemorySaverkeeps checkpoints in RAM, as the persistence docs say. Use a persistent checkpointer; for Postgres in FastAPI see the AsyncPostgresSaver page. - You don't need per-thread state at all. Compile without a checkpointer. In my runs
builder.compile()ran with no config.
Common questions
Is this a LangGraph bug?
No. A checkpointer stores state per thread and LangGraph refuses to guess which one. The [] wording and the LangServe failure are rough edges around version changes, but the fix is still to pass thread_id.
Should I pass thread_id or checkpoint_id?
thread_id. The message lists all three keys, but checkpoint_id picks a past checkpoint inside a thread (for time travel), and checkpoint_ns is set for subgraphs. For a normal call, thread_id is the one you need.
Can I use the user ID as the thread_id?
You can, but then each user has one endless thread. Most chat apps use one thread_id per conversation and keep what should outlive a conversation in a store or a memory layer.
Does create_agent need this too?
Yes, when you pass checkpointer=. Step 5 shows create_agent raising the same error with context= alone.
Related pages
- LangGraph memory explained: checkpointer vs store, and per-user memory across threads
- LangGraph AsyncPostgresSaver: fixing 'psycopg.OperationalError: the connection is closed' in FastAPI
- Persistent memory for a LangChain chatbot after LangChain 1.0
Sources
- github.com/langchain-ai/langchain/issues/34363 (opened 2025-12-15, closed 2025-12-15 as completed, 2 comments; the checkpointer error after
invoke(..., context=...)is quoted in the second comment; read 2026-10-30) - github.com/langchain-ai/langgraph/issues/5385 (opened 2025-07-07, closed 2025-07-20 as completed, 5 comments;
... 'configurable' keys: []after upgrading langgraph-checkpoint 2.0.26 to 2.1.0, LangServeRemoteRunnable;config_specs is emptycomment; read 2026-10-30) - github.com/ag-ui-protocol/ag-ui/issues/683 and github.com/CopilotKit/CopilotKit/issues/2719 (the same error on regenerate; both closed; read 2026-10-30), ag-ui-langgraph 0.0.35 and 0.0.47 source (read, not run)
- LangGraph docs: Persistence, docs.langchain.com/oss/python/langgraph/persistence (checkpointer vs store table, "Pass a
thread_idin graph config",InMemorySavernot persisting across restarts, UUID thread IDs; fetched 2026-10-30) - LangServe 0.3.1 and 0.3.3 source (
api_handler.py,per_req_config_modifier) and README (github.com/langchain-ai/langserve); PyPI release info for langgraph, langgraph-checkpoint and langserve (read 2026-10-30) - Offline runs against langgraph 0.4.10, 0.5.1 and 1.2.18 with langgraph-checkpoint 2.0.26, 2.1.0 and 4.3.1, langchain 1.4.7, LangServe 0.3.1 and 0.3.3 (2026-10-30); PyPI latest langgraph 1.2.20 / checkpoint 4.3.1 as of 2026-11-09 (not re-run)
- MemorySync LangGraph guide: docs.memorysync.io/guides/langgraph (fetched 2026-10-30)
Try it
For an overview of MemorySync, see the MemorySync homepage.
Next step: read the MemorySync LangGraph guide, then see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
About the author
- Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.
Comments
Join the discussionNo comments yet. Be the first to comment.
