How to Fix 'async event loop error when scaling' in LangGraph (Python)

By Cyprian AaronsUpdated 2026-04-21
async-event-loop-error-when-scalinglanggraphpython

What this error usually means

If you see async event loop error when scaling in LangGraph, you’re almost always hitting a mismatch between how your app is running and how LangGraph is being called. The failure usually shows up when you scale from a local script to FastAPI, Flask, Celery, Streamlit, Jupyter, or multiple concurrent requests.

The root issue is usually one of these: calling asyncio.run() inside an existing loop, reusing an async graph object across threads incorrectly, or mixing sync and async LangGraph APIs in the same execution path.

The Most Common Cause

The #1 cause is starting a new event loop from inside an already-running event loop.

This happens a lot when people wrap graph.ainvoke() in asyncio.run() inside FastAPI handlers, notebook cells, or background workers. LangGraph itself is fine here; the problem is the Python runtime.

Broken vs fixed pattern

BrokenFixed
asyncio.run() inside async codeawait the coroutine directly
Sync wrapper around async graph callNative async handler
Works locally, fails under loadStable under concurrency
# BROKEN
import asyncio
from langgraph.graph import StateGraph

def run_graph(graph, input_data):
    # This explodes if called from an existing event loop
    return asyncio.run(graph.ainvoke(input_data))

# Example error:
# RuntimeError: asyncio.run() cannot be called from a running event loop
# FIXED
from langgraph.graph import StateGraph

async def run_graph(graph, input_data):
    # Correct: reuse the current event loop
    return await graph.ainvoke(input_data)

If you’re using FastAPI, keep the endpoint async:

from fastapi import FastAPI

app = FastAPI()

@app.post("/chat")
async def chat(payload: dict):
    result = await graph.ainvoke(payload)
    return result

If you need sync code to call async LangGraph, isolate it at the boundary:

def handler_sync(graph, payload):
    # Only use this in pure sync entrypoints like CLI scripts
    return asyncio.run(graph.ainvoke(payload))

Other Possible Causes

1) Mixing invoke() with async nodes

If your graph contains async node functions but you call the sync API, LangGraph can surface runtime errors that look like event loop problems.

# BROKEN
async def enrich_state(state):
    return {"x": 1}

result = graph.invoke({"input": "hi"})  # sync call against async graph

# FIXED
result = await graph.ainvoke({"input": "hi"})

2) Running LangGraph inside Jupyter or IPython with asyncio.run()

Jupyter already has an active loop. Calling asyncio.run() there gives the classic error:

  • RuntimeError: asyncio.run() cannot be called from a running event loop
# BROKEN in notebook cells
import asyncio
result = asyncio.run(graph.ainvoke({"input": "hi"}))

# FIXED in notebook cells
result = await graph.ainvoke({"input": "hi"})

3) Creating one global client/session per thread incorrectly

If your nodes use HTTP clients or DB sessions that are not thread-safe, scaling requests can trigger loop-related failures downstream.

# BROKEN: shared client across concurrent tasks/threads
client = SomeAsyncClient()

async def node(state):
    return await client.fetch(state["id"])

Use per-request lifecycle management:

# FIXED: create within request scope or dependency scope
async def node(state):
    async with SomeAsyncClient() as client:
        return await client.fetch(state["id"])

4) Calling blocking code inside async nodes

A blocking function can stall the event loop and cause timeouts or misleading concurrency failures when traffic increases.

# BROKEN
import time

async def node(state):
    time.sleep(2)  # blocks the event loop
    return {"done": True}

Use non-blocking equivalents or offload to a thread:

# FIXED
import asyncio

async def node(state):
    await asyncio.sleep(2)
    return {"done": True}

If you must call CPU-bound or blocking code:

import asyncio

def blocking_work():
    ...

async def node(state):
    result = await asyncio.to_thread(blocking_work)
    return {"result": result}

How to Debug It

  1. Find the exact exception text

    • If you see RuntimeError: asyncio.run() cannot be called from a running event loop, it’s almost certainly nested event loops.
    • If you see RuntimeError: This event loop is already running, check notebooks and web frameworks first.
    • If you see LangGraph stack traces around ainvoke, inspect whether your caller is sync or async.
  2. Check which API you’re calling

    • Use invoke() only from pure sync code.
    • Use ainvoke() from async code.
    • Don’t mix them casually in wrappers.
  3. Trace the execution boundary

    • FastAPI route? Make it async def.
    • Celery task? Usually sync entrypoint; call async carefully.
    • Notebook cell? Use await, not asyncio.run().
  4. Reduce to a minimal repro

    • Remove all external services.
    • Keep one node.
    • Call one graph method.
    • If the error disappears, the bug is likely in your surrounding runtime or shared client setup.

Prevention

  • Keep a strict rule: sync entrypoints call invoke(), async entrypoints call ainvoke().
  • Never wrap LangGraph coroutines with asyncio.run() inside frameworks that already manage an event loop.
  • Make external clients and DB sessions lifecycle-aware; don’t share unsafe globals across concurrent requests.
  • Add one integration test for each runtime you deploy into: CLI, FastAPI, notebook, worker.

If you’re scaling LangGraph in production and this error appears only under load, treat it as an application boundary problem first, not a LangGraph bug. In most cases, fixing the async/sync split removes it immediately.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides