# LangGraph: fixing 'Checkpointer requires one or more of the following configurable keys: thread_id, checkpoint_ns, checkpoint_id'

*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:

```text
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](https://github.com/langchain-ai/langchain/issues/34363), where it came [from a call](https://github.com/langchain-ai/langchain/issues/34363#issuecomment-3659267674) that passed runtime `context=` (with a `session_id` in it) but no `config`. In LangGraph issue [#5385](https://github.com/langchain-ai/langgraph/issues/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](https://docs.langchain.com/oss/python/langgraph/persistence) 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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id) shows how to add long-term memory per user across threads with one middleware for `create_agent`. It doesn't replace the checkpointer or supply a `thread_id`, so you still need the fix below. When you're ready, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

## Status (Nov 9, 2026)

- [#34363](https://github.com/langchain-ai/langchain/issues/34363) (opened Dec 15, 2025, 2 comments) is closed. The maintainer reply was that the `context` parameters exist on recent versions ([reply](https://github.com/langchain-ai/langchain/issues/34363#issuecomment-3656172697)); the follow-up with the checkpointer error got no answer in the thread.
- [#5385](https://github.com/langchain-ai/langgraph/issues/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](https://github.com/langchain-ai/langgraph/issues/5385#issuecomment-3235771530)). 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](https://pypi.org/project/langgraph/)), 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:

```python
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:

1. **`context=` is not `config=`.** Runtime context is data your nodes and middleware read. The checkpointer never looks at it, even when your context class has a field named `session_id` or `thread_id`. Pass both if you use both.
2. **The check only asks for a non-empty `configurable`.** Put anything else in it and you skip the `ValueError`, then the saver fails on the missing key instead (`KeyError: 'thread_id'` with `InMemorySaver`). Same fix.
3. **A top-level `thread_id` worked.** LangChain Core's `ensure_config` moves unknown top-level config keys into `configurable`, so `{"thread_id": "abc"}` was saved under thread `abc`. I'd still write the `configurable` form, 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:

```python
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)}}
```

```python
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:

```python
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:

```python
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:

```text
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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id) 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):**

```bash
pip install "langchain-memorysync[langgraph]"
```

```python
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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id); 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](https://memorysync.hashnode.dev/langgraph-checkpointer-vs-store-cross-thread-memory).

**Try it:** MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id), 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: pass `configurable.thread_id`. If you're behind LangServe, see Step 4.
- **`KeyError: 'thread_id'` instead.** `configurable` was there but had no `thread_id` (for example only `user_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](https://github.com/ag-ui-protocol/ag-ui/issues/683) and [CopilotKit #2719](https://github.com/CopilotKit/CopilotKit/issues/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=None` or `""` (Step 3).
- **No error, but the conversation is gone after a restart.** `InMemorySaver` keeps checkpoints in RAM, as the persistence docs say. Use a persistent checkpointer; for Postgres in FastAPI see [the AsyncPostgresSaver page](https://memorysync.hashnode.dev/langgraph-asyncpostgressaver-connection-is-closed-fastapi).
- **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](https://memorysync.hashnode.dev/langgraph-checkpointer-vs-store-cross-thread-memory)
- [LangGraph AsyncPostgresSaver: fixing 'psycopg.OperationalError: the connection is closed' in FastAPI](https://memorysync.hashnode.dev/langgraph-asyncpostgressaver-connection-is-closed-fastapi)
- [Persistent memory for a LangChain chatbot after LangChain 1.0](https://memorysync.hashnode.dev/langchain-persistent-memory-memorysync)

## 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, LangServe `RemoteRunnable`; `config_specs is empty` comment; 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_id` in graph config", `InMemorySaver` not 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id).

**Next step:** read the [MemorySync LangGraph guide](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id), then [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpointer-requires-configurable-keys-thread-id). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

