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

  1. compile(…, store=InMemoryStore()) attaches the store to the graph
  2. A node can ask for it by adding a store parameter, and LangGraph passes it in automatically
  3. The node also reads user_id from config, so each user gets their own namespace
  4. store.put(namespace, key, value) saves a dictionary, and store.get(namespace, key) returns an item, whose data is in .value
  5. 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
  6. 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

  1. Run the first example with a third thread for Asha and confirm all three logs stay separate
  2. Extend the store example so “my name is X” is saved and recalled the same way as the favorite topic
  3. 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:

LangGraph Persistence and Checkpointers: Add Memory to Your Graph
LangGraph Human-in-the-Loop: Pause, Approve, and Resume
Studyopedia Editorial Staff
contact@studyopedia.com

We work to create programming tutorials for all.

No Comments

Post A Comment