Join & rejoin streams
Join and rejoin lets you disconnect from a running agent stream without stopping the agent, then reconnect to it later. The agent continues executing server-side while the client is away, and you pick up the stream exactly where you left off.
Interactive example: PatternEmbed
Note
This feature requires the LangGraph Agent Server. Run your agent locally with
langgraph devor deploy it to LangSmith to use this pattern.
Why join & rejoin?
Traditional streaming APIs tightly couple the client and server: if the client disconnects, the stream is lost. Join and rejoin breaks this coupling, enabling several important patterns:
- Network interruptions: mobile users moving between cell towers or Wi-Fi networks can seamlessly resume
- Page navigation: users navigating away from a chat page and returning later without losing progress
- Mobile backgrounding: apps suspended by the OS can rejoin the stream when foregrounded
- Long-running tasks: agents performing multi-minute operations (research, code generation, data analysis) where users don’t need to keep the page open
- Multi-device handoff: start a conversation on your phone, rejoin on your desktop
Core concepts
The join/rejoin pattern involves three key mechanisms:
| Method / Option | Purpose |
|---|---|
threadId | Bind the stream to the LangGraph thread you want to observe |
onThreadId | Persist newly-created thread IDs so a remount can reconnect |
stream.disconnect() | Leave the stream client-side while the agent keeps running server-side |
Remount with the same threadId | Reattach to in-flight work for that thread |
Note
Join/rejoin uses
stream.disconnect(), notstream.stop(). By default,stream.stop()cancels the active run: it disconnects the client and cancels the run on the server. For join/rejoin, callstream.disconnect()(alias forstop({ cancel: false })) so the agent continues processing while you are away.To cancel execution explicitly from app code, use
stream.stop()orclient.runs.cancel.
Setting up useStream
The key setup step is persisting threadId. When the component remounts with
the same thread ID, the stream attaches to the thread’s current state and any
in-flight run.
Info
The code examples use
useStream<typeof myAgent>for type-safe stream state. See Type inference for Python or JavaScript backends.
import { useStream } from "@langchain/react";
import { useCallback, useState } from "react";
function Chat() {
const [connected, setConnected] = useState(true);
const [mountKey, setMountKey] = useState(0);
const [threadId, setThreadId] = useState<string | null>(
() => sessionStorage.getItem("activeThreadId"),
);
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId,
onThreadId(id) {
setThreadId(id);
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
const disconnect = useCallback(() => {
void stream.disconnect();
setConnected(false);
}, [stream]);
const rejoin = useCallback(() => {
setMountKey((key) => key + 1);
setConnected(true);
}, []);
return (
<div key={mountKey}>
<ConnectionStatus connected={connected} />
<MessageList messages={stream.messages} />
<ChatControls
stream={stream}
threadId={threadId}
connected={connected}
onDisconnect={disconnect}
onRejoin={rejoin}
/>
</div>
);
}<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { ref } from "vue";
const connected = ref(true);
const mountKey = ref(0);
const threadId = ref<string | null>(sessionStorage.getItem("activeThreadId"));
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId,
onThreadId(id) {
threadId.value = id;
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
function disconnect() {
void stream.disconnect();
connected.value = false;
}
function rejoin() {
mountKey.value += 1;
connected.value = true;
}
</script>
<template>
<div :key="mountKey">
<ConnectionStatus :connected="connected" />
<MessageList :messages="stream.messages" />
<ChatControls
:stream="stream"
:threadId="threadId"
:connected="connected"
@disconnect="disconnect"
@rejoin="rejoin"
/>
</div>
</template><script lang="ts">
import { useStream } from "@langchain/svelte";
let connected = $state(true);
let mountKey = $state(0);
let threadId = $state<string | null>(sessionStorage.getItem("activeThreadId"));
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId: () => threadId,
onThreadId(id) {
threadId = id;
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
function disconnect() {
void stream.disconnect();
connected = false;
}
function rejoin() {
mountKey += 1;
connected = true;
}
</script>
<div key={mountKey}>
<ConnectionStatus {connected} />
<MessageList messages={stream.messages} />
<ChatControls
{threadId}
{connected}
onDisconnect={disconnect}
onRejoin={rejoin}
/>
</div>Angular
import { Component, signal } from "@angular/core";
import { injectStream } from "@langchain/angular";
@Component({
selector: "app-chat",
template: `
<connection-status [connected]="connected()" />
<message-list [messages]="stream.messages()" />
<chat-controls
[stream]="stream"
[threadId]="threadId()"
[connected]="connected()"
(disconnect)="disconnect()"
(rejoin)="rejoin()"
/>
`,
})
export class ChatComponent {
threadId = signal<string | null>(sessionStorage.getItem("activeThreadId"));
connected = signal(true);
mountKey = signal(0);
stream = injectStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId: this.threadId,
onThreadId: (id) => {
this.threadId.set(id);
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
disconnect() {
void this.stream.disconnect();
this.connected.set(false);
}
rejoin() {
this.mountKey.update((key) => key + 1);
this.connected.set(true);
}
}Submitting messages
Submit messages normally. The thread ID binding is what allows a later remount to reconnect to the same conversation:
stream.submit({ messages: [{ type: "human", content: text }] });Disconnecting from a stream
Call stream.disconnect() to leave the stream without cancelling the run. The agent continues processing server-side.
await stream.disconnect();
// equivalent to: await stream.stop({ cancel: false })
Do not use stream.stop() here — by default it cancels the run on the server.
After calling disconnect():
stream.isLoadingbecomesfalse- Your own
connectedflag should also becomefalse - The message list retains all messages received up to the disconnect point
- The agent continues running on the server
- No new messages are received until you rejoin
Rejoining a stream
Remount the stream consumer with the saved thread ID to reconnect. In React, the
demo bumps a mountKey; in other frameworks, use the equivalent remount or
conditional-render pattern:
setMountKey((key) => key + 1);
setConnected(true);After rejoining:
connectedbecomestrue- Any messages generated while disconnected are delivered
- New streaming messages resume in real-time
- If the agent is still running,
stream.isLoadingbecomestrue; if it has already finished, you receive the final state immediately
Best practices
- Use
disconnect()for join/rejoin,stop()to cancel: navigating away or backgrounding the app should callstream.disconnect(). A user-facing “Stop” or “Cancel” button should callstream.stop()(orclient.runs.cancel). - Always save the thread ID: without it, rejoining is impossible. Use both component state and persistent storage for resilience.
- Show clear connection state: users should always know whether they are receiving live updates or viewing a snapshot.
- Auto-rejoin on visibility change: use the Page Visibility API to automatically rejoin when the user returns to the tab.
- Set reasonable timeouts: if a rejoin attempt takes too long, fall back to fetching the thread history instead.
- Clean up stale threads: remove persisted thread IDs when the user starts over or the backend reports that the thread is unavailable.