Skip to content

Parallel Execution the Graph-Native Way, Not with asyncio.gather

Before you start

Prerequisite: Lesson 06 (multi-tool binding, ToolNode). This lesson assumes you can already bind and execute a single tool call; now you're running several at once. After this lesson, you can: fan out to multiple parallel nodes using LangGraph's own scheduling instead of asyncio, explain why that's preferred over asyncio.gather inside a node, and apply an already-established error-handling checklist instead of re-deriving one.

The pattern this course's own source material teaches, and why it's not the current recommendation

If you're comfortable with Python, "run several tool calls in parallel" reads like an asyncio.gather problem, and this course's own source material agrees: its parallel-execution exercise wraps several tool calls in asyncio.gather() inside one node's function body. It works. It also sidesteps the mechanism LangGraph itself already provides for exactly this case, and current guidance is specific about why that matters.

LangGraph executes in discrete super-steps. Nodes that get scheduled together in the same super-step already run concurrently, no asyncio code required, as long as the fan-out happens at the graph level: multiple edges from one source node, rather than multiple calls inside one node's body. The current recommended primitive for this is Send. This lesson checked that recommendation two independent ways this session: a LangChain forum thread on how to structure parallel nodes, and a cross-check against LangGraph's own current documentation via web search. Both agree Send is preferred over imperative concurrency inside a node specifically because manual subgraph-style calls inside a single node can hit MULTIPLE_SUBGRAPHS naming and namespace conflicts once checkpointing is involved. That's not a style preference; it's a documented failure mode the graph-native pattern avoids by construction.

Fan-out with Send

python
from langgraph.types import Send

def dispatch_searches(state: State) -> list[Send]:
    """Router function: fan out to a worker node once per query, in parallel."""
    return [
        Send("search_worker", {"query": q})
        for q in state["pending_queries"]
    ]

def search_worker(payload: dict) -> dict:
    """Runs once per Send, concurrently with every other invocation in the same super-step."""
    result = run_search(payload["query"])
    return {"results": [result]}  # merged via a reducer, one entry per parallel branch

Instead of returning a single node name the way a conditional edge does, the router returns a list of Send objects, each targeting the same node with different input. LangGraph schedules every one of them in the same super-step and runs them concurrently on its own, then merges their results back into state through a reducer once all branches finish, the same reducer discipline lesson 02 already covered, applied here to results arriving from parallel branches instead of sequential node calls.

Two ways to run three tool calls concurrently

Error handling: applying a checklist you already have

The source material's own error-handling exercise builds a categorization-and-routing system from scratch: error types, an error state field, routing logic based on error category. tools-memory-and-multi-agent-systems lesson 09 already worked out the same problem for external API calls, and it's worth applying directly rather than re-deriving: a timeout of at least 8 seconds, exponential backoff on 429 and 5xx responses, and an explicit error map (404 to NOT_FOUND, 429 to RATE_LIMITED, 5xx to UPSTREAM_ERROR) rather than letting a raw exception reach the model.

Step 1: Categorize at the source, not after the fact

Map the actual exception or status code to a named category the moment you catch it, inside the tool function itself. A node further downstream trying to guess an error's category from a generic exception message is working with less information than the tool already had.

Step 2: Route on category, using the same conditional-edge mechanism from lesson 04

A RATE_LIMITED error might route to a retry-with-backoff node; a NOT_FOUND might route straight to a "tell the user nothing matched" response. This is exactly lesson 04's routing mechanism, applied to error categories instead of message classifications.

Step 3: Keep the retry budget in state, with a reducer if more than one branch can increment it

A retry_count field that multiple parallel Send branches might each increment needs the same reducer discipline lesson 02 covered for any field more than one execution path can touch concurrently.

Quick check — You need to run five independent API calls concurrently inside a LangGraph agent, then combine their results. What's the current recommended approach?

What this buys you before lesson 08

You can now fan out to parallel work using LangGraph's own scheduler instead of asyncio, and you have a reusable error-handling checklist instead of one built from scratch per project. Lesson 08 covers the last gap this course's original source material never addressed at all: pausing a graph mid-run for a human decision, and what actually happens to state while it waits.

Continue to Lesson 08

Human-in-the-loop and production deployment: the interrupt mechanism, checkpointing across sessions, and the deployment concerns this course's source material's own "Deploy & Scale" section only ever named in four bullet points.

Have a question about this lesson?

Reply here and it goes straight to Rod. Same as replying to one of his emails.