TechByteByByte

Streaming, Observability, and Debugging a Running Graph

Every failure mode from the last module raises the same real question: which node ran, what state went in, what came out, why this edge? This module answers it — five stream modes, astream_events, and real debugging discipline.

#LangGraph#Streaming#Observability#Debugging

Recall Module 25’s own recurring question, hiding underneath every failure mode it covered: which node actually ran, what state went in, what came out, and why did the graph take this specific edge instead of another. This module gives you the real, concrete tools to answer that question — for a live, running graph, and for one you’re debugging after the fact.

Example 1: stream_mode="values" — full state after every step

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    count: int

def step_one(state: State) -> dict:
    return {"count": state["count"] + 1}

def step_two(state: State) -> dict:
    return {"count": state["count"] + 10}

builder = StateGraph(State)
builder.add_node("step_one", step_one)
builder.add_node("step_two", step_two)
builder.add_edge(START, "step_one")
builder.add_edge("step_one", "step_two")
builder.add_edge("step_two", END)
graph = builder.compile()

for chunk in graph.stream({"count": 0}, stream_mode="values"):
    print(chunk)

"values" gives you the entire state after each node finishes — genuinely the clearest way to watch a graph’s real, complete state evolve, one full snapshot at a time.

Example 2: stream_mode="updates" — just what changed

for chunk in graph.stream({"count": 0}, stream_mode="updates"):
    print(chunk)

"updates" shows only what each node actually returned — {"step_one": {"count": 1}}, then {"step_two": {"count": 11}} — genuinely useful when you specifically want to isolate which node changed which field, rather than re-reading the full state every time.

Example 3: stream_mode="messages" — token-level streaming

Recall streaming from your LangChain course — this is the same real idea, now at the graph level.

from langgraph.graph import StateGraph, START, END, MessagesState
from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4o-mini")

def call_model(state: MessagesState) -> dict:
    return {"messages": [model.invoke(state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
graph = builder.compile()

for chunk, metadata in graph.stream({"messages": [{"role": "user", "content": "Explain RAG in one sentence."}]}, stream_mode="messages"):
    print(chunk.content, end="", flush=True)

Genuinely the same token-by-token responsiveness from your LangChain course’s own streaming module, now flowing through a graph’s own node structure rather than a single, isolated model call.

Example 4: astream_events — the richest real, per-step feed

Recall this exact API from your LangChain course, applied there to create_agent. It works identically here, because — recall Module 15 — that was always this same underlying engine.

import asyncio

async def main():
    async for event in graph.astream_events({"count": 0}, version="v2"):
        if event["event"] == "on_chain_start" and event["name"] in ("step_one", "step_two"):
            print(f"→ Starting: {event['name']}")
        if event["event"] == "on_chain_end" and event["name"] in ("step_one", "step_two"):
            print(f"→ Finished: {event['name']}, output: {event['data'].get('output')}")

asyncio.run(main())

This is the most granular real view available — every node’s start, every node’s end, every intermediate tool call inside it, streamed live as the graph actually runs.

Example 5: streaming into subgraphs

Recall Module 22’s subgraphs — by default, a subgraph’s internal steps stay hidden from the parent’s stream. subgraphs=True opens that visibility directly.

for chunk in main_graph.stream({"query": "test"}, stream_mode="updates", subgraphs=True):
    print(chunk)

Without subgraphs=True, you’d only ever see the subgraph’s node ("billing", for instance) as one opaque step. With it, you see every real, individual step happening inside that subgraph too — genuinely important once your graph has grown modular enough that debugging requires looking inside a specific piece, not just at the top level.

Example 6: post-hoc debugging with get_state_history

Recall this directly from Module 17 — real, historical debugging, after a run has already finished.

history = list(graph.get_state_history({"configurable": {"thread_id": "debug-session"}}))
for snapshot in reversed(history):
    print(f"After {snapshot.metadata.get('step')}: {snapshot.values}")

This is the real, complete answer to “why did this specific execution take the path it did” — not a live stream, but a genuine, persisted record you can inspect at any point afterward, exactly like reviewing a flight recorder after the fact.

The real debugging discipline, stated plainly

flowchart TD
    A["A graph misbehaved.\nWhat actually happened?"] --> B[Which node ran?]
    B --> C[What state went into it?]
    C --> D[What did it actually return?]
    D --> E[Which edge got taken next, and why?]
    E --> F["Use astream_events live,\nor get_state_history after the fact,\nto answer all four"]

Recall Module 2’s own opening promise — graph.invoke() was never magic, and this diagram is the concrete, practical proof: every one of these four questions has a real, mechanical, inspectable answer, never a guess.

Why this matters at genuinely production scale

It’s worth being direct about this — Module 25’s real Portkey data showed that failures across 650+ real organizations were routine, not exceptional. Observability is precisely what turns “something went wrong” into “node X received field Y as null, because node Z’s reducer silently dropped it” — the real, specific, fixable diagnosis a production team actually needs, rather than a vague report that “the agent behaved oddly” with no way to reproduce or genuinely understand why.

Common mistakes worth avoiding

Debugging exclusively with print() statements scattered through node functions. Recall this module’s own real tools — stream_mode="updates" and astream_events give you this same visibility, structured and consistent, without needing to modify and redeploy your actual node code every time you want to inspect something new.

Forgetting subgraphs=True and assuming a subgraph’s own internal failure is invisible. Recall Example 5 — the failure is genuinely there, in the subgraph’s own execution; you simply weren’t watching for it without this flag.

Treating a live stream as a substitute for get_state_history. A live stream only shows you what’s happening right now; genuine post-hoc debugging of a run that already finished — or crashed — needs the real, persisted history, not a live feed you’d have needed to be watching at the exact right moment.

What you should take away from this module

  • stream_mode="values", "updates", and "messages" each answer a genuinely different real question — full state, just the diff, or token-level text.
  • astream_events gives the richest, most granular real view of a graph’s live execution, node by node.
  • subgraphs=True opens visibility into a subgraph’s own internal steps, not just its outer node.
  • get_state_history is the real, post-hoc equivalent — reconstructing exactly what happened, after the fact, from persisted, genuine data.

Where this goes next

The next module covers Testing — writing real, layered tests for nodes, routers, reducers, loops, interrupts, subgraphs, and entire graphs, so you catch these same real problems before a real user ever encounters them.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed