It started with a Slack alert at 3:14 AM. The billing dashboard for our LLM provider showed a sharp, vertical spike in token usage. By the time the engineer on call logged into the console, a single autonomous agent pipeline had consumed over four million tokens in less than three hours.
A simple, unattended cron job designed to update API client libraries caused the spike, not a database migration or an attack. The agent had gotten stuck in an infinite execution loop, burning through 245 consecutive retries before a hard timeout in the CI pipeline finally killed the process. This highlights the need for active llm token cost optimization to mitigate runaway inference budgets.
This incident highlights a major vulnerability in autonomous agent architectures: the lack of deterministic boundaries. When we give LLMs the power to call tools and evaluate their own failures, we risk creating feedback loops that feed on their own errors. This is the autopsy of that failure, the mechanics of how the agent trapped itself, and the guardrails needed to prevent it from happening again.
The system was designed to automate SDK maintenance. Every night, a cron job pulled the latest OpenAPI specification from our core services. If it detected changes, it spun up an LLM agent powered by a frontier model.
We gave the agent a clear system prompt to review the API schema changes, update the TypeScript SDK code, run the test suite, and submit a pull request with the fixes.
To perform these tasks, the agent had access to four tools:
- A file reader to inspect the codebase.
- A file writer to modify code.
- A terminal executor to run local tests.
- A web client tool to make HTTP requests to live staging environments to verify integration.
The orchestration framework was built using a standard loop. The agent generated an action, the runtime executed the tool, the output was appended to the conversation history, and the agent decided on the next action. If a tool returned an error, the agent was instructed to analyze the failure and correct its approach.
This design worked well for minor changes, like adding a field to a payload. When an upstream service introduced a breaking change, the agent's self-correction mechanism turned into a liability.
The loop began when the core API service removed a deprecated endpoint: /v1/users/profile/billing. The agent updated the SDK code but found that one integration test failed with a 404 Not Found error.
Instead of stopping and reporting the failure, the agent tried to debug the issue. The terminal output showed the test failure, including the raw HTTP response. The agent analyzed this error and assumed the endpoint had not been deleted, but rather moved or renamed.
First, the agent decided that the path in the test config was wrong. It used its web client tool to query the staging server, guessing a new path: /v1/users/billing-profile. The staging server returned a 404.
The agent read the 404 response. Instead of recognizing that the endpoint was gone, it concluded that the endpoint might require a different path structure. It tried /v1/billing/profile. The server returned another 404.
The agent then assumed the issue might be authentication. It modified the request headers, adding a dummy bearer token, and retried the same path. The server returned a 401 Unauthorized.
To the agent, the change from 404 to 401 looked like progress. It reasoned that the path was correct, but the credentials were bad. It spent the next fifty iterations trying to generate valid test tokens, querying different authentication endpoints, and modifying its request payloads.
When those attempts failed, it went back to path guessing. It tried variations like /api/v1/users/profile/billing, /v1/users/profile/billing-info, and /v2/users/profile/billing.
What made this worse was the staging server's configuration. Because it was a development environment, it returned detailed HTML error pages containing stack traces and internal directory paths. The agent parsed this HTML, found references to internal controller files like UserBillingController.java, and began guessing paths based on Java class names.
Each failure was fed back into the prompt context. The conversation history grew larger with every iteration. Because the context window was large enough to hold the history, the model did not run out of memory. It just kept reading its past failures, generating new hypotheses, and executing new web requests.
The orchestration wrapper had an automatic retry policy for network timeouts, but this was a logical loop. The tools were executing successfully and returning valid HTTP responses. The agent was choosing to retry. By the time the CI environment hit its three-hour execution limit, the agent had completed 245 cycles of tool execution, log analysis, and prompt generation.
To prevent these loops, we have to look at why LLMs fail to break out of them. Traditional software loops fail because of infinite logic conditions. Agentic loops fail because of semantic traps.
The first issue is the helpfulness bias. LLMs are trained to satisfy the user's prompt. If you tell an agent to fix the test suite, it will try to find a way to make the tests pass. The model does not naturally conclude that a task is impossible or that the underlying system has changed in a way that requires human intervention. It views every error as a puzzle to be solved, leading to endless iterations.
The second issue is context inflation. As the loop continues, the conversation history accumulates errors, code snippets, and stack traces. This accumulation degrades the model's reasoning capability. The attention mechanism gets distracted by the noise of past attempts, making the agent more likely to generate low-quality actions or hallucinated paths. The agent loses track of the original goal and starts focusing entirely on fixing the immediate error in front of it.
The third issue is the lack of state tracking in basic agent runtimes. The agent framework treated each step as a fresh generation based on the history. It did not track that it had tried /v1/users/billing-profile fifty steps ago. The model lacks the short-term memory structures needed to detect repetitive behavior across a long sequence of actions. For systems requiring robust state, benchmarking agent memory helps evaluate how different vector stores and context systems perform under load.
The fourth factor was the model configuration. The pipeline used a temperature setting of 0.7, which was intended to help the agent find creative workarounds for build issues. Instead, the high temperature caused the model to continually invent new, non-existent endpoints. At temperature 0, the model would have likely repeated the exact same path and hit a local cache or a simple rate limit. At 0.7, it had just enough creativity to keep the loop fresh and expensive.
We redesigned our agent architecture to prevent these loops. The goal was to build boundaries that do not rely on the LLM's own judgment to stop.
First, we added execution budgets. You cannot rely on time limits alone, as token processing speeds vary. We set hard limits on both the maximum number of tool executions and the total token spend per run. For our SDK generator, we set a limit of twenty tool calls. If the agent does not reach a terminal state within twenty steps, the runner terminates the process and flags it for review.
Second, we restricted the tools. The agent previously had access to a generic web client tool that could make arbitrary HTTP requests. We replaced this with a structured API client tool that validates paths against the OpenAPI schema before making the request. If the agent tries to call an endpoint that does not exist in the schema, the tool executor rejects the call locally without hitting the network or charging tokens.
Third, we implemented a loop detection system in the orchestration runner. The runner hashes the state of each tool call, including the tool name and the arguments. If the runner detects that the exact same tool call with the same arguments has been executed three times, it halts the execution.
Here is a Python implementation of this loop detector:
import hashlib
import json
class AgentCircuitBreaker:
def __init__(self, max_repeats=3):
self.max_repeats = max_repeats
self.action_history = {}
def _generate_hash(self, tool_name: str, arguments: dict) -> str:
serialized = json.dumps(
{"tool": tool_name, "args": arguments},
sort_keys=True
)
return hashlib.sha256(serialized.encode('utf-8')).hexdigest()
def register_action(self, tool_name: str, arguments: dict) -> bool:
action_hash = self._generate_hash(tool_name, arguments)
self.action_history[action_hash] = self.action_history.get(action_hash, 0) + 1
if self.action_history[action_hash] >= self.max_repeats:
return False
return TrueThis code tracks the actions at the runner level. If the agent tries to run execute_http_request with the path /v1/users/billing-profile more than twice, the register_action method returns False, and the runner terminates the loop.
Another change was modifying how error messages are returned to the agent. Originally, we sent the raw error payload back to the model. This gave the LLM too much raw material to hallucinate from.
We now sanitize the errors. If a tool call fails, the runner returns a structured, high-level summary. For example, instead of returning a full stack trace and raw HTML for a 404 error, the runner returns: Error: Endpoint not found. Verify the path against the local schema.
This keeps the context window clean and directs the agent toward local validation rather than remote guessing. If the error persists, the runner injects a system message: Action failed repeatedly. Do not retry this action. If no alternative path exists, terminate and report the issue.
Standard application monitoring tools are not built to detect semantic loops. They track HTTP request rates and database latency, but they do not understand the intent behind an agent's actions. To catch these issues early, we had to build custom observability layers.
We integrated tracing that logs every step of the agent's execution path. Each step is tagged with a run ID, the tool used, the token cost, and the semantic similarity of the current step's output to the previous three steps. If the semantic similarity of the agent's thoughts remains above 90% for three consecutive iterations, our monitoring system flags the run as a potential loop.
This telemetry is exported to a central dashboard. We can now see the execution graph of every active agent in real time. If an agent starts branching out into deep, repetitive sub-graphs, we can kill the run manually before it exhausts its token budget.
For critical pipelines, we added a human-in-the-loop validation step. If the agent hits a warning threshold, such as ten tool calls or two repeated errors, the runner pauses the execution and sends a message to a Slack channel. While this adds safety, manual gates are not perfect; studies show humans miss one-third of security threats when validating agent commands. If a developer does not respond within fifteen minutes, the runner defaults to aborting the run. This prevents runaway token spend while allowing complex tasks to proceed with human oversight.
The 245-retry incident was a reminder that autonomous agents are still software systems. They require the same operational guardrails as any other automated workflow. Giving an LLM access to loop constructs and tools without strict, deterministic limits is a recipe for high API bills and unstable deployments.
By separating the execution logic from the LLM's reasoning loop, we can build agents that are both helpful and safe. The model should focus on proposing actions, while the runtime environment remains responsible for enforcing limits, validating inputs, and pulling the emergency brake when things go off course.



