langchain-ai/langchain · critical · RecursionError

Recursion limit reached when invoking {self} with input {val

Error message

Recursion limit reached when invoking {self} with input {value}.

What it means

Async twin of the sync recursion check: in `RunnableLambda._ainvoke`, when the wrapped coroutine returns a `Runnable`, it is awaited via `output.ainvoke(...)` with `recursion_limit - 1`. At limit 0 the chain of async runnables-returning-runnables is halted with `RecursionError`. This protects async agent-style pipelines from unbounded delegation.

Source

Thrown at libs/core/langchain_core/runnables/base.py:5278

                    if output is None:
                        output = chunk
                    else:
                        try:
                            output = output + chunk  # type: ignore[operator]
                        except TypeError:
                            output = chunk
        else:
            output = await acall_func_with_variable_args(
                cast("Callable[..., Any]", afunc), value, config, run_manager, **kwargs
            )
        # If the output is a Runnable, invoke it
        if isinstance(output, Runnable):
            recursion_limit = config["recursion_limit"]
            if recursion_limit <= 0:
                msg = (
                    f"Recursion limit reached when invoking {self} with input {value}."
                )
                raise RecursionError(msg)
            output = await output.ainvoke(
                value,
                patch_config(
                    config,
                    callbacks=run_manager.get_child(),
                    recursion_limit=recursion_limit - 1,
                ),
            )
        return cast("Output", output)

    @override
    def invoke(
        self,
        input: Input,
        config: RunnableConfig | None = None,
        **kwargs: Any | None,
    ) -> Output:
        """Invoke this `Runnable` synchronously.

View on GitHub (pinned to e32fa9a52e)

Solutions

  1. Make each delegation step strictly consume the input so recursion terminates.
  2. Increase `recursion_limit` in the config if depth is expected: `await chain.ainvoke(x, {'recursion_limit': 100})`.
  3. Replace recursive delegation with an explicit `while` loop or a state machine (`RunnableBranch`, LangGraph).
  4. Log inputs per level to spot non-shrinking inputs.

Example fix

# before
async def route(x):
    return handler_runnable  # delegates forever on same input

# after
async def route(x):
    if x['done']:
        return x
    return await handler_runnable.ainvoke(x)  # consume and finish
Defensive patterns

Strategy: validation

Validate before calling

async def ainvoke_bounded(chain, x, max_depth: int = 50):
    return await chain.ainvoke(x, config={'recursion_limit': max_depth})

Try / catch

try:
    out = await chain.ainvoke(x)
except RecursionError as e:
    if 'Recursion limit reached' in str(e):
        out = await chain.ainvoke(x, config={'recursion_limit': 100})
    else:
        raise

Prevention

When it happens

Trigger: An `afunc` (or async `func`) that returns a `Runnable` which again returns a `Runnable`, indefinitely; async routers where the delegate never changes the input; `config={'recursion_limit': n}` smaller than the delegation depth.

Common situations: Async agent loops built from `RunnableLambda` routing to the next step; deep async chains in FastAPI handlers; migrating sync recursion-limited code to async and re-hitting the limit.

Related errors


AI-assisted analysis of langchain-ai/langchain@e32fa9a52e (2026-08-14). Data as JSON: /api/errors/0b4682a2909d1a41. Report an issue: GitHub.