langchain-ai/langchain · critical · RecursionError

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

Error message

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

What it means

In `RunnableLambda._transform` (sync streaming), if the function's output is itself a `Runnable`, the lambda streams from that runnable with `recursion_limit - 1`; at limit 0 a `RecursionError` is raised. This bounds sync streaming pipelines where lambdas keep returning runnables, mirroring the invoke-path guard.

Source

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

                    output = chunk
                else:
                    try:
                        output = output + chunk
                    except TypeError:
                        output = chunk
        else:
            output = call_func_with_variable_args(
                self.func, final, config, run_manager, **kwargs
            )

        # If the output is a Runnable, use its stream output
        if isinstance(output, Runnable):
            recursion_limit = config["recursion_limit"]
            if recursion_limit <= 0:
                msg = (
                    f"Recursion limit reached when invoking {self} with input {final}."
                )
                raise RecursionError(msg)
            for chunk in output.stream(
                final,
                patch_config(
                    config,
                    callbacks=run_manager.get_child(),
                    recursion_limit=recursion_limit - 1,
                ),
            ):
                yield chunk
        elif not inspect.isgeneratorfunction(self.func):
            # Otherwise, just yield it
            yield cast("Output", output)

    @override
    def transform(
        self,
        input: Iterator[Input],
        config: RunnableConfig | None = None,

View on GitHub (pinned to e32fa9a52e)

Solutions

  1. Terminate delegation: invoke/absorb the returned runnable inside the lambda instead of returning it.
  2. Raise `recursion_limit`: `chain.stream(x, config={'recursion_limit': 100})`.
  3. Model the hand-off as an explicit sequence or loop rather than nested runnable returns.
  4. Verify each level's output type — only return a `Runnable` when you intend transparent delegation.

Example fix

# before
fn = RunnableLambda(lambda chunks: (yield next_runnable))  # yields a Runnable

# after
fn = RunnableLambda(lambda chunks: (yield from next_runnable.stream(consume(chunks))))
Defensive patterns

Strategy: validation

Validate before calling

def stream_bounded(chain, x, max_depth: int = 50):
    yield from chain.stream(x, config={'recursion_limit': max_depth})

Try / catch

try:
    for chunk in chain.stream(x):
        yield chunk
except RecursionError as e:
    if 'Recursion limit reached' in str(e):
        yield from chain.stream(x, config={'recursion_limit': 100})
    else:
        raise

Prevention

When it happens

Trigger: A generator-function lambda yielding a `Runnable` as a chunk; `stream()` on a chain whose lambda returns a runnable that returns a runnable, deeper than `recursion_limit`; explicit low `recursion_limit` in streaming config.

Common situations: Streaming routers that hand off to the next runnable per chunk; migrating invoke-based delegation code to `stream()` and keeping infinite hand-off; deep streaming chains exceeding the default limit.

Related errors


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