Course Content
LangChain Mastery
7 sections · 109 lessons
How do you implement memory for multi-user conversations in LangChain?
What you need to know
"Multi-user" means two problems
- Isolation — user A must never see user B's history. This is a security requirement.
- Scale — thousands of users across many worker processes must each get their own history, fast.
Both are solved by the same design: identifiers from auth, state in a shared store.
Keys and namespaces
| Memory | Key | Example |
|---|---|---|
| One conversation | thread_id | "u-881:c-17" |
| A user's long-term facts | store namespace | ("users", "u-881") |
| Shared team knowledge | store namespace | ("org", "acme-legal", "notes") |
A user can have many conversations; a thread belongs to exactly one user. Keep a table of (user_id, thread_id) so you can list and delete a user's threads.
With create_agent
1from dataclasses import dataclass2from langchain.agents import create_agent34@dataclass5class Ctx:6 user_id: str78agent = create_agent(model, tools=TOOLS, checkpointer=saver, store=store,9 context_schema=Ctx)1011def handle(request, conversation_id: str, text: str):12 user_id = request.auth.user_id # from the session, not the body13 thread_id = f"{user_id}:{conversation_id}"14 if not db.thread_belongs_to(thread_id, user_id): # ownership check15 raise PermissionError("Not your conversation")16 return agent.invoke(17 {"messages": [{"role": "user", "content": text}]},18 config={"configurable": {"thread_id": thread_id}},19 context=Ctx(user_id=user_id),20 )Tools read runtime.context.user_id to choose the store namespace, so a tool physically cannot be pointed at another user's data by the model.
Legacy: RunnableWithMessageHistory
Older code solved the same problem with history_factory_config, which replaced the single session_id with several config fields, such as user_id and conversation_id, passed to the history factory as arguments. The wrapper is deprecated since langchain-core 1.3.3; the design rule — build the key from authenticated identifiers — carries over unchanged to thread_id.
Classic bugs
- A module-level
store = {}dict in a multi-worker server: each worker has its own copy, and memory seems to come and go. - A cached chain or memory object reused across requests, so one user's state leaks into the next request.
- Taking
user_idfrom the JSON body: anyone can read anyone's thread by changing a number. - Vector memory search without a user filter.
A real-life example
A law firm's contract assistant is used by 400 lawyers across 3 teams. Each lawyer has private research threads, and each team shares a notes namespace, ("team", team_id, "notes"), that any team member's agent can search.
During a security review, a tester changed the conversation_id in the API call to one from another lawyer. Because the thread_id was built from the authenticated user ID plus the conversation ID, and the ownership check ran before invoke, the request was rejected. The review found one real issue: a team-notes search tool used a team_id argument supplied by the model. The team moved team_id into runtime context from the user's directory record, and added an automated test where user A tries to reach user B's threads, store items and team notes in 20 different ways.
Follow-up questions to expect
- "Where should the user ID come from?" — The authenticated session or token on the server, then passed in config and runtime context — never from the model or the request body.
- "How do you share memory between users safely?" — A separate namespace for the shared data with explicit membership checks, read-only by default.
- "How do you scale it?" — Stateless workers, a shared Postgres or Redis backend with a connection pool, and indexes on thread and namespace keys.