07 Oct LangGraph Threads: Handle Multiple Conversations and Users
In Chapter 13, you used a thread_id to load a saved conversation. This chapter goes deeper: how threads keep conversations apart, how to design thread IDs for real apps, and how to share information across threads.
What is a thread?
A thread is one conversation with its own saved state. Think of it as one chat window:
- Same thread_id means the same conversation, so history is loaded
- Different thread_id means a fresh conversation, so nothing is shared
Thread "asha-1": "My name is Asha" -> "What's my name?" -> "Asha" Thread "asha-2": "What's my name?" -> "I don't know"
Threads stay separate
A quick check with a tiny graph, no LLM needed:
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())
asha = {"configurable": {"thread_id": "asha-1"}}
ravi = {"configurable": {"thread_id": "ravi-1"}}
graph.invoke({"log": ["asha"]}, asha)
graph.invoke({"log": ["ravi"]}, ravi)
graph.invoke({"log": ["asha again"]}, asha)
print(graph.get_state(asha).values)
print(graph.get_state(ravi).values)
Output:
{'log': ['asha', 'visit', 'asha again', 'visit']}
{'log': ['ravi', 'visit']}
Each thread has its own history, and neither one leaks into the other.
Designing thread IDs for a real app
A thread ID is just a string, so you choose the pattern. Common approaches:
- One thread per user, for example “user-42”. Use it when each person has one ongoing chat.
- User plus chat, for example “user-42-chat-7”. Use it when users can have many chats, like ChatGPT-style sidebars.
- Random ID, for example str(uuid.uuid4()). Use it for anonymous or one-off sessions.
Creating a fresh conversation for a user:
import uuid
def new_thread(user_id: str) -> dict:
chat_id = uuid.uuid4().hex[:8]
return {"configurable": {"thread_id": f"{user_id}-{chat_id}"}}
config = new_thread("user-42")
print(config)
Sample output:
{'configurable': {'thread_id': 'user-42-3f9a1c2b'}}
Because LangGraph doesn’t keep a “list of my threads” for you, your app should store a table of each user’s thread IDs (for example, in your own database), so you can show a chat list.
Thread memory has a limit
Everything in a thread is remembered only inside that thread. Say a user tells the bot “I prefer short answers” in one chat, then opens a new chat. The new thread knows nothing about it.
That is where the store comes in.
Cross-thread memory with a Store
- Short-term memory lives in the checkpointer (state per thread) and covers one conversation.
- Long-term memory lives in the store and is shared across all of a user’s threads.
A store is a simple key-value database. Items live under a namespace, a tuple like (user_id, “profile”), which works like a folder path.
from typing import TypedDict
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
class State(TypedDict):
message: str
reply: str
def remember(state: State, config: RunnableConfig, *, store: BaseStore):
user_id = config["configurable"]["user_id"]
namespace = (user_id, "profile")
prefix = "my favorite topic is "
if state["message"].startswith(prefix):
topic = state["message"].removeprefix(prefix)
store.put(namespace, "topic", {"value": topic})
return {"reply": f"Saved: {topic}"}
item = store.get(namespace, "topic")
if item:
return {"reply": f"Your favorite topic is {item.value['value']}"}
return {"reply": "I don't know your favorite topic yet"}
builder = StateGraph(State)
builder.add_node("remember", remember)
builder.add_edge(START, "remember")
builder.add_edge("remember", END)
graph = builder.compile(
checkpointer=InMemorySaver(),
store=InMemoryStore(),
)
chat_1 = {"configurable": {"thread_id": "chat-1", "user_id": "asha"}}
chat_2 = {"configurable": {"thread_id": "chat-2", "user_id": "asha"}}
other = {"configurable": {"thread_id": "chat-3", "user_id": "ravi"}}
print(graph.invoke({"message": "my favorite topic is Python"}, chat_1)["reply"])
print(graph.invoke({"message": "what is my favorite topic?"}, chat_2)["reply"])
print(graph.invoke({"message": "what is my favorite topic?"}, other)["reply"])
Output:
Saved: Python Your favorite topic is Python I don't know your favorite topic yet
How it works
- compile(…, store=InMemoryStore()) attaches the store to the graph
- A node can ask for it by adding a store parameter, and LangGraph passes it in automatically
- The node also reads user_id from config, so each user gets their own namespace
- store.put(namespace, key, value) saves a dictionary, and store.get(namespace, key) returns an item, whose data is in .value
- In chat-2 (a brand-new thread), the state is empty, yet the topic is found, because the store is shared across threads for asha
- ravi has a different namespace, so he sees nothing
The config now carries two IDs with different jobs: thread_id picks the conversation, and user_id picks whose long-term memory to use.
Searching a namespace
To list everything saved for a user:
items = store.search((user_id, "profile"))
for item in items:
print(item.key, item.value)
Storage options
InMemoryStore is lost when the program stops, just like InMemorySaver. For production, use a database-backed store such as Postgres, so long-term memory survives restarts. Chapter 23 touches on deployment.
Security: keep users apart
- Never trust a thread ID or user ID sent straight from a browser. Someone could type another person’s ID. In a real app, take the user ID from your login system on the server.
- Namespace by user so one user can’t read another user’s memories
- Don’t store secrets or sensitive personal data in a store unless you have planned for protecting it
- Give users a way to clear their data, since saved memories are personal information
Common mistakes
- Reusing one thread ID for everyone: all users would share a single conversation
- Expecting memory to carry to a new thread: use a Store for cross-thread facts
- Forgetting user_id in the config: the node fails with a KeyError when it reads it
- Putting the store in invoke(): it belongs in compile()
- A namespace that isn’t a tuple: use (user_id, “profile”), not a plain string
- Saving non-dictionary values: store values must be dictionaries
Summary
- A thread is one saved conversation, chosen by thread_id
- Different thread IDs never share state
- Build thread IDs from your app’s users, such as user plus chat, and keep your own list of them
- The checkpointer holds short-term memory per thread, and the store holds long-term memory across threads
- Namespaces like (user_id, “profile”) keep each user’s memories separate
Practice
- Run the first example with a third thread for Asha and confirm all three logs stay separate
- Extend the store example so “my name is X” is saved and recalled the same way as the favorite topic
- Use store.search() to print everything saved for one user
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:
- Generative AI Tutorial
- AI Ethics
- Machine Learning Tutorial
- Deep Learning Tutorial
- Ollama Tutorial
- Retrieval Augmented Generation (RAG) Tutorial
- ChatGPT Tutorial
- Microsoft Copilot Tutorial
No Comments