Every call so far has blocked
Since Lesson 1, .invoke() has meant "send this, and freeze the entire program until the answer comes back." For one call at a time, that's fine. For several independent calls, it's wasteful: each one spends most of its time just waiting on the network, and there's no reason those waits can't overlap. .ainvoke() is the async twin of .invoke(), and asyncio.gather is what lets several of them wait at the same time instead of one after another.
Nodes don't need to change
def chatbot(state: MessagesState) -> dict: response = model.invoke(state["messages"]) return {"messages": [response]}This node is written exactly like every other node in the course, a plain, synchronous function. LangGraph handles the async plumbing from the outside, calling it via .ainvoke()/.astream() doesn't require rewriting node functions as async def themselves.
The async call itself
async def ask_async(question: str) -> str: result = await app.ainvoke({"messages": [HumanMessage(question)]}) return result["messages"][-1].textSame shape as .invoke(), same result structure, the only difference is await in front of it. Inside an async def function, await pauses this coroutine without blocking the rest of the program, other coroutines can keep running during that wait.
Sequential vs. concurrent, measured
async def run_sequential() -> float: start = time.perf_counter() for question in QUESTIONS: await ask_async(question) return time.perf_counter() - start
async def run_concurrent() -> float: start = time.perf_counter() await asyncio.gather(*(ask_async(question) for question in QUESTIONS)) return time.perf_counter() - startrun_sequential awaits each call before starting the next, so total time is roughly the sum of all three. run_concurrent starts all three ask_async coroutines essentially at once via asyncio.gather, so total time is closer to the slowest single call, not the sum of all of them.
Checkpoint
.ainvoke()/.astream(): async counterparts of.invoke()/.stream(), same result shapes, non-blocking while waiting.asyncio.gather(...): runs several coroutines concurrently, waiting on all of them together instead of one at a time.- nodes stay synchronous: you don't need
async defnode functions to benefit from.ainvoke()at the graph level. - why concurrency helps here specifically: each call spends most of its time waiting on the network, exactly the kind of wait that overlaps well with other waits.
If anything here still feels unclear, ask before moving to Lesson 34.