LangChain AgentExecutor infinite loop — max_iterations hit, cost blowup (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Agent infinite loop
LangChain Agents · Execution Severity: High HTTP n/a

LangChain AgentExecutor — infinite loop, max_iterations reached

Agents that loop until the hard iteration cap are the classic LangChain production incident. Cost balloons, latency explodes, and users see either a timeout or a useless generic response.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: An AgentExecutor loop hits max_iterations when the model keeps calling tools without ever emitting a final answer. Root causes: tool returns confuse the model, prompt implicitly encourages more tool use, tool errors surface as retry-inducing text, or the final-answer format the model expects is unreachable. Fix by (a) setting max_iterations to a sensible cap (5-15 typical), (b) adding a clear "when to stop" instruction in the system prompt, and (c) making tool error paths explicit and non-retriable.

Real error messages you'll see

These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Max iterations reached — generic output
AgentExecutor stopped due to iteration limit or time limit.
# Final output: some generic "I was unable to complete the task" message
Same tool called repeatedly
[iter 1] search(query="X")
[iter 2] search(query="X")
[iter 3] search(query="X")
# Tool result is ambiguous or empty; model keeps trying
Cost explosion in one session
# LangSmith trace shows one AgentExecutor call: 34 iterations,
# 187,000 total input tokens, $2.14 on Opus
# Root cause: retrieval tool returned empty; model looped searching alternatives

Reference

Loop-inducing tool return patterns

Tool returnModel reaction
Empty stringRetries with variations — "no results found?"
Error message as textRetries or reformulates
Ambiguous partial dataCalls more tools to disambiguate
Very long unstructured textSummarizes internally, then retries
NoneCoerced to "None" text; model confused

AgentExecutor termination controls

ParameterPurpose
max_iterationsHard cap on tool call iterations
max_execution_timeWall-clock cap (seconds)
early_stopping_method"force" (stop hard) or "generate" (ask for final answer)
handle_parsing_errorsHow to react when the model output does not parse
return_intermediate_stepsLog tool_use / tool_result pairs

Root causes, ranked by frequency

Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    Tools return empty or ambiguous results. Empty retriever, tool that returns 0 results without a clear signal.
  • 18%
    Prompt implicitly rewards more tool use. "Use tools thoroughly" or "make sure you have all info" leads to over-searching.
  • 14%
    Tool errors returned as normal text. Model treats the error message as a hint and retries with variations.
  • 10%
    Ambiguous user query. Model tries different interpretations, each with a tool call.
  • 8%
    No max_iterations set. Default is 15 which is fine for many cases; but for cost-sensitive apps, lower it.
  • 7%
    Tool schema too permissive. Optional args let the model retry with different combinations.
  • 7%
    Race condition in async tools. Duplicate concurrent tool calls; each returns different partial data; model tries to reconcile.
  • 10%
    No termination criterion in the prompt. Missing "if you have enough info, produce the final answer" instruction.

Fixes — copy-paste solutions

Fix #1

Set explicit iteration cap and early stopping method

The bare minimum to prevent runaway cost.

Configure max_iterations, max_execution_time, and early_stopping_method="generate" on every AgentExecutor. Never ship agents without limits.

bounded_agent.py
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

model = ChatOpenAI(model="gpt-4o-mini")

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are helpful. Use tools to gather information. "
     "When you have enough information to answer, PROVIDE THE FINAL ANSWER "
     "immediately — do not keep calling tools."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(model, tools, prompt)

# Every executor needs bounds
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=8,                    # never more than 8 tool calls
    max_execution_time=60,               # 60 seconds wall clock
    early_stopping_method="generate",    # ask model for final answer if we cap out
    handle_parsing_errors=True,          # gracefully handle parse errors as tool errors
    return_intermediate_steps=True,      # for observability
)

result = executor.invoke({"input": "What is the weather in Karachi?"})
print("output:", result["output"])
print(f"steps taken: {len(result['intermediate_steps'])}")
The distinction between early_stopping_method="force" and "generate" is important. "force" returns a generic message; "generate" gives the model one final chance to answer from what it already has.
Fix #2

Make tool returns unambiguous — success and failure both crisp

Well-designed returns are the biggest loop preventer.

When a tool succeeds, return crisp structured data. When it fails, return an explicit "no results" or "error" signal that the model will not try to work around. Never return generic error messages inline with data.

crisp_tool_returns.py
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Search for information about a topic.

    Args:
        query: What to search for.

    Returns:
        A structured result or a clear "no results" signal.
    """
    results = do_search(query)

    if not results:
        # ❌ WRONG — vague signal invites retry
        # return "No results."
        # return ""
        # return "I could not find anything."

        # ✓ RIGHT — explicit, machine-parseable "empty" signal
        return f"NO_RESULTS for query: {query!r}. Do not retry this query."

    if len(results) > 20:
        # Truncate and be explicit
        return "\n".join(f"{i+1}. {r}" for i, r in enumerate(results[:20])) + \
               f"\n\n[Showing top 20 of {len(results)} total results]"

    return "\n".join(f"{i+1}. {r}" for i, r in enumerate(results))


@tool
def fetch_page(url: str) -> str:
    """Fetch and return the text content of a web page.

    Args:
        url: Full URL to fetch.

    Returns:
        The page text, or an explicit error indicator.
    """
    try:
        return fetch_url(url)  # returns page text
    except TimeoutError:
        # ❌ WRONG — generic error text; model may retry
        # return "Error: timeout occurred while fetching."
        # ✓ RIGHT — explicit non-retry signal
        return f"FETCH_ERROR: TIMEOUT for {url}. The page cannot be reached. Do not retry."
    except HTTPError as e:
        return f"FETCH_ERROR: HTTP {e.status_code} for {url}. Do not retry."
    except Exception as e:
        return f"FETCH_ERROR: {type(e).__name__} for {url}. Do not retry."
The phrase "Do not retry" in error returns is a powerful signal to modern LLMs. Combined with a clear failure marker (e.g. FETCH_ERROR), it reliably prevents retry loops.
Fix #3

Add cost circuit-breaker in a callback

Hard cost cap independent of iteration count.

Iteration cap alone is not enough — a single iteration on a large-context model can be expensive. Add a token counter callback that raises when a threshold is crossed.

cost_circuit_breaker.py
from langchain_core.callbacks import BaseCallbackHandler
from langchain.agents import AgentExecutor

class CostLimitCallback(BaseCallbackHandler):
    """Raise when accumulated token cost exceeds a threshold."""

    def __init__(self,
                 input_token_price_per_mtok: float,
                 output_token_price_per_mtok: float,
                 hard_cap_usd: float):
        self.in_price = input_token_price_per_mtok / 1_000_000
        self.out_price = output_token_price_per_mtok / 1_000_000
        self.hard_cap = hard_cap_usd
        self.spent = 0.0

    def on_llm_end(self, response, **kwargs):
        usage = response.llm_output.get("token_usage", {}) if response.llm_output else {}
        in_tokens = usage.get("prompt_tokens", 0) or usage.get("input_tokens", 0)
        out_tokens = usage.get("completion_tokens", 0) or usage.get("output_tokens", 0)
        self.spent += in_tokens * self.in_price + out_tokens * self.out_price
        print(f"[cost] +${in_tokens*self.in_price + out_tokens*self.out_price:.4f} "
              f"total ${self.spent:.4f}")
        if self.spent > self.hard_cap:
            raise RuntimeError(
                f"Cost cap ${self.hard_cap:.2f} exceeded (spent ${self.spent:.2f}). "
                f"Aborting agent."
            )

# Bind to executor
cost_cb = CostLimitCallback(
    input_token_price_per_mtok=15.0,   # e.g. Opus 4.7 input
    output_token_price_per_mtok=75.0,  # e.g. Opus 4.7 output
    hard_cap_usd=0.50,                 # kill anything spending > $0.50 on one request
)

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    max_iterations=15,
    max_execution_time=120,
    early_stopping_method="generate",
    callbacks=[cost_cb],
)

# The callback raises inside the agent loop if cost is exceeded
try:
    result = executor.invoke({"input": "very complex query"})
except RuntimeError as e:
    print(f"Aborted: {e}")
The dollar figures for token pricing change over time — parameterize them and update in one place. For multi-model agents, use a per-model price table.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Every AgentExecutor needs max_iterations, max_execution_time, and early_stopping_method set.
  • System prompt must explicitly instruct: "when you have enough info, produce the final answer".
  • Tools return crisp, explicit success or failure signals — never generic errors that read like retry hints.
  • Add a per-request cost circuit breaker via callback; abort at a threshold.
  • Log intermediate_steps to observability — repeated identical tool calls indicate looping.
  • Alert on any agent run above N iterations (typically 10) to catch drift over time.
  • Load-test agents with adversarial queries designed to induce looping; measure worst-case cost.

Frequently asked questions

5-10 for most production agents; 15 for research-style workflows; 20+ only for genuine long-horizon agents with strong controls. Start low and raise if you see legitimate incomplete answers.
LangGraph is the newer, more flexible primitive for agent workflows. AgentExecutor is still supported and simpler for basic tool-calling agents. For complex control flow (loops, retries, human-in-the-loop) prefer LangGraph.
It checks between iterations — an in-flight LLM call typically completes before the timeout kicks in. For hard latency SLA, add a per-call timeout at the model client level (ChatOpenAI(timeout=30)).
Not with built-in AgentExecutor config. Track it in a custom callback that inspects intermediate_steps and raises when the same tool is called more than N times.
force returns a generic "hit iteration limit" message. generate calls the model one more time with the accumulated context and asks for a best-effort final answer. generate almost always gives a better user experience.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.