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.
Quick fix (TL;DR)
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.
AgentExecutor stopped due to iteration limit or time limit. # Final output: some generic "I was unable to complete the task" message
[iter 1] search(query="X") [iter 2] search(query="X") [iter 3] search(query="X") # Tool result is ambiguous or empty; model keeps trying
# 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 return | Model reaction |
|---|---|
| Empty string | Retries with variations — "no results found?" |
| Error message as text | Retries or reformulates |
| Ambiguous partial data | Calls more tools to disambiguate |
| Very long unstructured text | Summarizes internally, then retries |
None | Coerced to "None" text; model confused |
AgentExecutor termination controls
| Parameter | Purpose |
|---|---|
max_iterations | Hard cap on tool call iterations |
max_execution_time | Wall-clock cap (seconds) |
early_stopping_method | "force" (stop hard) or "generate" (ask for final answer) |
handle_parsing_errors | How to react when the model output does not parse |
return_intermediate_steps | Log 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
Set explicit iteration cap and early stopping method
Configure max_iterations, max_execution_time, and early_stopping_method="generate" on every AgentExecutor. Never ship agents without limits.
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'])}")
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.Make tool returns unambiguous — success and failure both crisp
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.
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."
FETCH_ERROR), it reliably prevents retry loops.Add cost circuit-breaker in a callback
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.
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}")
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Every AgentExecutor needs
max_iterations,max_execution_time, andearly_stopping_methodset. - 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_stepsto 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
ChatOpenAI(timeout=30)).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.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.