Streaming

Updated 5 interview angles 4 min read source
On this page8
  1. The stream modes
  2. Token streaming versus step streaming
  3. Custom progress from inside a node
  4. Subgraphs
  5. Streaming does not make it faster
  6. Wiring it to a client
  7. Related
  8. Interview angle

Streaming

An agent that takes forty seconds and shows nothing feels broken. Streaming is a product requirement before it is a technical one, and LangGraph gives you several kinds of it — the skill is picking the right one.

The stream modes

python
mode = "updates"
for chunk in graph.stream(inputs, stream_mode=mode):
    print(chunk)
Mode Emits
values the full state after each step
updates only what each step changed
messages LLM tokens as they generate
custom whatever a node chooses to emit
checkpoints checkpoint events
tasks task start and finish
debug checkpoints plus tasks, with metadata

updates is the one to default to. It gives you the node name and its state delta, which is what a progress UI needs — “retrieving”, “reranking”, “writing” — without shipping the whole state on every step.

values gets expensive. The full state after every step means large payloads if state holds documents; useful for debugging, wasteful for a UI.

checkpoints and tasks require a checkpointer, since there is nothing to report otherwise.

Token streaming versus step streaming

These answer different questions and people conflate them.

text
messages  -> "the answer is being typed"
updates   -> "we are on step 3 of 7"

messages gives you the token-by-token effect users expect from a chat box. updates tells them the system is working through stages. A long agent run needs both: stage progress while tools run, then token streaming when the final answer generates.

Pass a list to get both, and branch on the chunk type:

python
for chunk in graph.stream(
    inputs, stream_mode=["updates", "messages"]
):
    ...

Gotcha: messages mode only works with LangChain-integrated models, because the tokens are surfaced by the integration. A node calling a raw provider SDK produces nothing there — emit your own via custom instead.

Custom progress from inside a node

When a node does long work with no model call — parsing a hundred documents, say — nothing appears on the stream. Emit progress explicitly:

python
from langgraph.config import get_stream_writer

def ingest(state: State) -> dict:
    write = get_stream_writer()
    for i, doc in enumerate(state["docs"]):
        write({"stage": "parse", "done": i})
        parse(doc)
    return {"parsed": True}

This is the escape hatch that makes progress reporting work for arbitrary work, and it is what you use when messages cannot apply.

Subgraphs

By default a subgraph’s internal steps do not appear. Pass subgraphs=True to include them, and each chunk carries the namespace it came from so you can tell whose step it is. See Multi-agent and subgraphs.

Whether you want them is a product question: a user watching a support agent does not need to see a sub-agent’s tool calls, but an engineer debugging it does.

Streaming does not make it faster

The point worth making in an interview, because it separates a product answer from a naive one:

“Streaming changes perceived latency, not actual latency. The run takes just as long. What changes is that the user sees progress at 200ms instead of staring at a spinner for forty seconds — and separately, they can tell early that it is going wrong and stop it.”

That second clause is the underrated half. A visible plan lets a user abort a run that has misunderstood them, which saves both money and their patience.

For work too long to hold a connection at all, streaming is not the answer — a job with a status URL is. See Long-running agent jobs.

Wiring it to a client

Server-sent events are the usual transport: one-way, plain HTTP, reconnects built into the browser API. WebSockets only earn their place when the client also needs to send mid-run — for example to answer an interrupt.

python
async def sse(request):
    async def gen():
        async for chunk in graph.astream(
            inputs, stream_mode="updates"
        ):
            yield f"data: {json.dumps(chunk)}\n\n"
    return StreamingResponse(
        gen(), media_type="text/event-stream"
    )

Two practical details: disable proxy buffering or nothing arrives until the end, and decide what a disconnect means — either the run continues server-side and the client can reattach by thread id, or it is cancelled. That choice belongs in the design, not in a default.

Interview angle 5

  • “How do you stream from a graph?” - stream() with a mode. updates for step progress, messages for token-level output; they answer different questions and a long run usually needs both.
  • “What’s the difference between values and updates?” - values emits the whole state after each step, updates only the delta. values gets expensive when state holds documents, so updates is the default for a UI.
  • “A node does slow work with no model call — how do you show progress?” - emit it explicitly with the stream writer. messages mode only produces tokens from LangChain-integrated models, so custom emission is the escape hatch.
  • “Does streaming make it faster?” - no, it changes perceived latency. The underrated half is that visible progress lets a user abort a run that has misunderstood them, which saves money as well as patience.
  • “SSE or WebSocket?” - SSE unless the client sends mid-run, which mainly means answering an interrupt. And decide explicitly what a disconnect does: continue server-side and allow reattach by thread id, or cancel.