Under the hood: Pregel, super-steps, channels

LangGraph · Expert · 15 min

Understanding the execution model explains confusing errors and lets you design graphs that are fast and correct.

A compiled StateGraph runs on LangGraph's Pregel engine, named after a Google system for processing large graphs in rounds. Each state key is stored in a channel, and each node is an actor that reads channels and writes to them.

Execution proceeds in super-steps:

1. Plan: pick every node whose inputs changed in the last step (or that was targeted by an edge, Command or Send). 2. Execute them all in parallel. Writes are buffered, so nodes in the same step don't see each other's updates. 3. Update: apply all buffered writes to channels, through reducers. Save a checkpoint. Repeat until nothing is scheduled.

Consequences:

  • Two parallel nodes writing the same key without a reducer raise InvalidUpdateError ("INVALID_CONCURRENT_GRAPH_UPDATE").
  • A super-step is transactional: if one parallel node fails, none of that step's writes are applied, though successful siblings' results are kept as pending writes so they aren't redone on retry.
  • recursion_limit counts super-steps, not nodes.
  • Branches of different lengths reach a join at different steps; use defer=True on the join node to make it wait until all other pending work is done.

Checkpoint cost. By default each checkpoint stores full channel values. For long threads with growing message lists, LangGraph 1.2 added DeltaChannel (beta), which stores only the change per step plus periodic snapshots. Switching a persisted channel between delta and normal storage is not backward compatible, so plan migrations.

Other performance tools: cache_policy=CachePolicy(ttl=...) with compile(cache=InMemoryCache()) to skip repeated expensive nodes, max_concurrency to limit parallel tasks, and durability="exit" for short runs that don't need mid-run recovery.

Example

import operator
from typing import Annotated, TypedDict
from langgraph.cache.memory import InMemoryCache
from langgraph.graph import StateGraph, START, END
from langgraph.types import CachePolicy


class State(TypedDict):
    q: str
    hits: Annotated[list[str], operator.add]


def fast(s: State):  return {"hits": ["fast"]}
def slow1(s: State): return {"hits": ["slow1"]}
def slow2(s: State): return {"hits": ["slow2"]}
def join(s: State):  return {"hits": [f"joined {len(s['hits'])}"]}


b = StateGraph(State)
b.add_node(fast)
b.add_node(slow1, cache_policy=CachePolicy(ttl=60))    # reuse result for 60 s
b.add_node(slow2)
b.add_node(join, defer=True)                           # wait for all branches
b.add_edge(START, "fast"); b.add_edge(START, "slow1")  # step 1: both in parallel
b.add_edge("slow1", "slow2")                           # step 2
b.add_edge("fast", "join"); b.add_edge("slow2", "join")
b.add_edge("join", END)
graph = b.compile(cache=InMemoryCache())
print(graph.invoke({"q": "x", "hits": []})["hits"])
# ['fast', 'slow1', 'slow2', 'joined 3']  (join ran once, after both branches)

Remember

  • Pregel runs nodes in super-steps: plan, execute in parallel, apply writes
  • Writes in a step are buffered and merged via reducers; steps are transactional
  • recursion_limit counts super-steps; defer=True delays joins
  • DeltaChannel (beta), caching and max_concurrency tune performance

Check yourself

  1. Two nodes run in the same super-step. Does the second see the first's state update?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.langchain.com/oss/python/langgraph/pregel