Pattern

Agents need a harness: the cost runaway hiding in your agent loop

· 6 min read · AI Operations Services · Part 3

Most of the agent failures I worry about don’t throw an error. That’s exactly what makes them expensive.

I’ve spent this year operating LLM agents on AWS Bedrock (Strands on AgentCore, real users at the other end) and there’s one failure mode I want every team shipping agents to design against before they meet it. When it hits, nothing pages. No 5xx, no alarm, no red tick in the channel. The service just quietly spends money until someone happens to look at the bill.

I’m not going to name the company, and I’ve changed the numbers to protect them. The shape is what matters, and the shape is real. Picture a customer research agent: you give it a question and it works the problem on its own until it hands back an answer.

”Until done” is doing a lot of work

It loops the way these agents do: think, search, read, critique, repeat, until it decides it’s done. That last clause is carrying the entire system on its back.

Agentic patterns are loops with a stopping condition the model controls. ReAct, plan-act-observe, self-critique, reflexion: same skeleton. And a loop whose exit condition depends on the model deciding it’s satisfied has a failure mode a for loop doesn’t: what happens when “done” never comes?

It doesn’t crash. A non-terminating agent loop doesn’t throw. It just keeps going, and every lap is another model call you pay for.

In this case it took one ambiguous request. The agent kept critiquing its own answer, never satisfied, and racked up more than 1,400 tool calls on a single question, re-billing the model every single time. No exception was raised, because nothing was broken. It was doing precisely what it was told: keep going until you’re happy. It never got happy.

Every dashboard stayed green

Here’s the part that should bother you. While that was happening, every operational dashboard was healthy.

  • HTTP 200 on every call.
  • p99 latency, normal.
  • CPU and memory, fine.
  • Error rate, zero. Uptime, 100%.

If you’d paged the on-call engineer they’d have glanced at the dashboards and gone back to bed. Because traditional APM (Datadog, New Relic, CloudWatch out of the box) answers one question: is the service up? It has no opinion on is the agent doing something insane?

The agent returned a perfect 200 every time, all the way to a $27-an-hour burn rate. Annualised, that one stuck agent was on a ~$230k trajectory. It was found nine days later, on the finance bill, not by anyone watching the system, because the signals people watch never moved.

The signals that did move (tokens per turn, dollars per hour, tool calls per request) weren’t on anyone’s dashboard. That’s not a monitoring gap you patch after the fact. It’s the whole category of thing you have to decide to watch.

It’s a category, not a bug

The tempting response is to fix the agent’s prompt and move on. That treats a category as a one-off.

Every agentic pattern being shipped right now is a loop, and a loop step with no terminal condition is unbounded spend waiting for the wrong input. It isn’t specific to research agents, or to Bedrock, or to any one framework. The moment you let a model decide when it’s finished, you’ve taken on this risk. So the fix can’t be a better prompt. It has to be structural.

The harness

The word I use for the structural fix is a harness. Not a cage. A harness is the thing that lets you take the lead off and actually let the agent run, because you know it can’t bolt into the neighbour’s yard. Three parts.

1. Bound the loop. Cap the iterations, per turn, and stop when the cap is hit. The trick is that “iterations” has more than one meaning, so bound more than one thing. On one workload I watched a model burn a turn not by calling tools but by regenerating the same reasoning over and over. The tool-call cap never tripped because it wasn’t calling tools. So the guard bounds tool calls, reasoning restarts, and wall-clock, and trips on whichever comes first:

def loop_guard_tripped(tool_calls, elapsed_s, reasoning_blocks,
                       max_tool_calls=12, max_seconds=120, max_reasoning=6):
    if tool_calls > max_tool_calls:
        return "tool_calls"
    if reasoning_blocks > max_reasoning:   # catches the no-tool regeneration loop
        return "reasoning"
    if elapsed_s > max_seconds:
        return "elapsed"
    return None

Set the ceilings from your own traffic. Measure the heaviest legitimate turn and leave headroom above it, so the guard only ever fires on genuine runaways. When it trips, stop the turn gracefully and hand back what you have. A capped answer beats a 44-minute hang that ends in an error.

2. Force a terminal state. Bounding one turn isn’t enough if the thing that re-drives the agent will just re-drive it forever. Anything with a retry (a queue, a poller, a sweeper) needs a give-up. Give the work a counter, increment it before the risky call so you’re counting attempts and not successes, and once it crosses N, flip it to a terminal state and stop. That single integer turns a worst case of “re-bill forever” into “N attempts, then done”, roughly a dollar instead of a $230k trajectory.

3. Watch the money. The reason it ran for nine days is that the one signal that would have caught it (spend) wasn’t being watched in a way that could alarm. Bedrock gives you tokens, not dollars, so synthesise the dollars: a CloudWatch math expression that turns tokens-per-model into an estimated dollars-per-hour, with a threshold. That fires in minutes, not on a month-end invoice. And make the alarm loud. Point it at a channel a human actually reads. An alarm with no subscriber is a diary entry.

The takeaway

A loop that never terminates is a loop that spends forever, and it will do it while your uptime dashboard is green. Bound the loop. Force a terminal state. Watch the cost. That’s the harness, and building it is the difference between letting an agent run and hoping an agent behaves.

If you’re shipping agents this year, this is the failure mode I’d design against first. It’s cheap to prevent and genuinely expensive to discover.


I write about operating AI in production: one lesson a week. If this was useful, the rest of the series is on rajmurugan.com.

Newsletter

Infrequent, high-signal posts on AWS + AI engineering — directly to your inbox.

Comments

  1. Loading comments…

Comments are reviewed before they appear.