Support request: v2 protocol event replay on reconnect/first-subscribe degrades UI state (self-hosted LangGraph + @langchain/react useStream)
Description
We’re migrating our chat frontend to @langchain/react’s useStream (v2 streaming protocol) against a self-hosted LangGraph Platform deployment, and we’re seeing a family of problems that all trace back to the same protocol behaviour: the server replays the thread’s full buffered event history to every newly-opened subscription, and the client has no reliable way to distinguish that replay from live events.
The reason we are noticing this problem is we are trying to migration to @lanchain/react.
The user-visible impact on a page refresh of an existing thread:
- State rewind: on the first
submit()after a refresh (the deferred root pump’s first open), the replay re-emits old runs’valuessnapshots. Each one replaces the projected root state, so our UI visibly reverts — chat title back to its initial value, task sections and artefacts/sidebar collapsing — before walking forward through history to the current state. - Loading/“thinking” indicator flicker and premature settle: replayed terminal lifecycle events from prior runs settle the loading state of the newly submitted run and trigger repeated subscription pause/resume cycles. On video we can watch the thinking indicator appear, vanish 4 seconds later (while the run is still executing server-side), and flicker with each replayed terminal. This looks like exactly what’s described in langgraphjs#2609.
- Delivery latency: the new run’s events queue behind the replay of all prior runs (we’ve measured 10–27 s of lag between server emission and client delivery mid-turn).
Versions
Backend (self-hosted):
| Component | Version |
|---|---|
| Docker base image | langchain/langgraph-api (dated tag -20260710), Python 3.13 |
langgraph-api |
0.10.3 (behaviour also verified unchanged in the 0.12.2 wheel and the 0.14.0.dev3 pre-release) |
langgraph (core) |
1.2.2 (also verified against 1.2.11, latest) |
langgraph-checkpoint / -postgres |
4.1.1 / 3.1.0 |
langchain-core |
1.4.0 |
langgraph-cli / langgraph-runtime-inmem (local dev) |
0.4.28 / 0.30.2 — all issues reproduce identically on the local dev server |
Frontend:
| Component | Version |
|---|---|
@langchain/react |
1.0.28 |
@langchain/langgraph-sdk |
1.9.27 (findings re-verified against the 1.9.29 tarball — the relevant modules are unchanged) |
| React | 19 |
What we’ve noticed in our investigation
We instrumented the wire (every protocol event at ThreadStream’s intake, subscription pause/resume, projection decisions). Findings, with code references:
- The step-based staleness guard can never engage on Python backends.
RootMessageProjection.applyValuestreats a snapshot as a stale replay only when it carries a checkpointsteplower than the max applied (addOnly). Steps are paired from companioncheckpoints-channel events — but those are never emitted for Python graphs:langgraphcore (through 1.2.11) emits the legacyCheckpointPayloadshape (config/metadata/values/next/parent_config, no top-levelid), whilelanggraph_api.event_streaming.session’s checkpoints intake requiresisinstance(data.get("id"), str)and silently drops everything else (verified throughlanggraph-api0.14.0.dev3). We measured 0checkpointsevents out of 583 protocol events. Net effect: every replayedvaluessnapshot is step-less and applied wholesale. - Even when
addOnlyengages, it only protects themessageskey — both branches ofapplyValuesspread the incoming stale snapshot over the rest of the state ({ ...nextValues, [messagesKey]: messages }), so non-message state rewinds regardless. - Replayed terminal lifecycle events affect the current run (the #2609 class): they settle loading state and each schedules
#scheduleTerminalPause, which pauses all non-lifecycle subscriptions (we logged 14 pauses / 6 resumes in an 18-second window).SubscriptionHandle.pause()resolves pending iterators withdone: true, so consumer loops exit and delivery becomes dependent on re-iteration after resume. - Transport reconnection gaps: the
sincecursor is only sent on the first connection attempt (reconnects replay from seq 0 — absorbed by per-subscriptionevent_iddedupe, but costly), and a clean stream close terminatesopenEventStream’s read loop permanently — reconnection exists only in the error path, which would affect deployed environments behind load balancers/proxies that idle-close connections. - Message ordering: a turn’s messages arrive out of persisted order (the final message’s stream starts first; narration and tool/artefact messages are inserted before it at persist time). We’ve adapted our client to adopt the persisted order at run completion.
Our architecture
A single Python LangGraph graph on self-hosted langgraph-api; React SPA (React Router, SPA mode). The route loader fetches threads.getState() + latest run for hydration, then:
const stream = useStream<AgentState>({
threadId, // from the route
client, // langgraph-sdk Client (authenticated)
assistantId,
initialValues, // loader's getState() values
onThreadId, onCreated, onCompleted,
});
// stream.values → app state (title, task sections, artefacts, sidebar viewing state)
// stream.messages → transcript
// plus custom channels via the thread handle (chain-of-thought, artefact
// content deltas, analytics …) emitted server-side with stream_mode="custom":
useChannelEffect(stream, ["custom:fd-state", "custom:fd-artefact-content", …], handler, { replay: false });
On onCompleted we re-fetch getState() and treat it as the authoritative snapshot. Non-message app state is driven directly from stream.values, which is why replayed snapshots are so visible for us.
Summary — what we’re really asking
Our core question is not about any single internal detail. It’s:
What is the intended/supported way to handle page refreshes and SSE reconnection when the protocol replays buffered events?
Concretely, for an app whose UI is driven by stream.values and stream.messages:
- On a page refresh of an existing thread (idle, or with a run in flight): what should the client do so that hydration + the subsequent replay converge cleanly — is there a combination of
useStreamoptions, hydration strategy, or subscribe parameters we should be using that we’ve missed? Are clients expected to treat replayed events as authoritative, or to ignore them in favour ofgetState()— and if the latter, how should a client distinguish replayed events from live ones? - On reconnection (network blip, proxy/LB closing the stream, or a stream that ends cleanly): what’s the recommended pattern for rejoining the live stream without re-consuming — or being visibly affected by — the full replay, and without losing events emitted while disconnected?
Does any of this involve having to upgrade, or are there common patterns that I am missing to combat these problems we are facing?
Thanks for any help in advance, it is much appreciated.