Your Agent Loop Needs a Stop Condition

·6 min read·Updated
ai-agentsllmreliabilitycostmemoryarchitecture

An agent is a loop: call the model, run whatever tool it asked for, feed the result back, repeat until it says it's done. The termination condition is a judgement made by the model, which means there isn't one — and a loop that doesn't stop on a small container fails in three ways at once. It spends real money on every lap, its message history grows until the container hits its hard memory limit and gets killed, and the platform restarts it, so it starts again. Nothing here will notice: there's no health probe, and a busy process looks exactly like a working one. Four limits fix it — a maximum iteration count, a wall-clock deadline, a spend budget, and repeat detection — and all four belong in your code before the agent runs unsupervised.

Why agent loops don't terminate

None of these are exotic. They're the normal failure modes:

  • A tool keeps failing and the model keeps retrying it. An error string reads as "try again", so it does, with the same arguments, forever.
  • Two tools undo each other. Write a file, read it, decide it's wrong, write it again. Each step is locally reasonable.
  • "Done" was never defined. If the task is vague, the model has no criterion to check against, so it keeps looking for more to do.
  • Context grows and comprehension drops. Twenty iterations in, the history is mostly tool output, and the model is less able to remember what it was asked. Longer loops make themselves worse.

The common thread is that the model is deciding when to stop, and it's deciding from inside the situation that's going wrong.

Limit 1 — a maximum iteration count

The simplest and most effective. Pick a number, count laps, stop.

for step in range(MAX_STEPS):     # not `while True`
    ...
else:
    raise AgentBudgetExceeded("hit step limit")

Set it from what the task actually needs. If your agent normally finishes in four steps, ten is generous and fifty is a bug that hasn't happened yet. Resist making it configurable and large — a big limit is the same as no limit, just slower.

When you hit it, fail loudly and say what happened. A partial result with "I ran out of steps" is far more useful than a silent stop, both for the user and for you reading logs later.

Limit 2 — a wall-clock deadline

Iterations aren't a good proxy for time. Five steps that each call a slow model can take longer than fifty fast ones, and an SDK's default timeout can be ten minutes per call.

Take a timestamp before the loop and check it every lap. If someone is waiting on the result, the deadline should be shorter than their patience, not shorter than your timeout.

This matters more here than on a bigger machine, because your CPU allocation is a time budget — an agent grinding away in the background is taking slices from everything else your app is trying to do.

Limit 3 — a spend budget per task

Iteration count doesn't bound cost either, because the context grows each lap. Ten iterations with a small history is cheap; ten with a large one is not, and the cost curve is closer to quadratic than linear.

Track tokens as you go — the usage numbers come back on every response — and stop when a single task crosses a ceiling you chose. That's a real limit rather than an inferred one.

Then set a spend limit at the provider as well. That's the backstop your own code can't defeat, and it's the only one that still works when the bug is in the counting. Worth doing before you need it, alongside knowing what the API side of an AI app actually costs.

Limit 4 — repeat detection

The cheapest signal that a loop has stopped making progress: the same tool called with the same arguments twice in a row.

Hash the tool name and its arguments, keep the last few, and if you see a repeat, break — or better, tell the model it just did that and ask for something different. One retry after a transient failure is fine; three identical calls is not a plan.

This catches the failure mode that step limits handle slowly and expensively. A loop stuck on a failing tool will happily burn all fifty of your allowed steps; repeat detection ends it on the third.

The memory problem nobody expects

Here's the one specific to running in a container with a hard cap.

Your message history grows every single iteration, and it holds every tool result — file contents, API responses, search results. That's the working set, and your RAM limit is a hard ceiling rather than a suggestion: cross it and the process is killed outright, which surfaces as an app that vanished for no visible reason.

Three things keep it bounded:

  • Cap the history length. Keep the system prompt, the original task and the last N exchanges. Drop the middle.
  • Truncate tool output before it enters the history. A tool that returns a 2 MB file should hand back the first few KB and a note, not the file.
  • Summarise instead of accumulating. Replace ten old exchanges with one paragraph of what was learned.

None of this is optional at 0.25 GB. It's barely optional at 2 GB.

Restarts make an unbounded loop worse

This is the part worth thinking about carefully, because it turns a bounded problem into an unbounded one.

The advice for a long-running agent is to persist the task before you start it, so a restart resumes rather than drops the work. That's right — but combined with a loop that has no stop condition, it's a machine for spending money. The agent loops, exhausts memory, gets killed, the container restarts, it reads the unfinished task, and begins again. Forever, without anyone touching anything.

So persist an attempt count alongside the task, not just the task. Increment it before you start, and after a small number of attempts mark the task failed and stop trying. A task that has crashed three times is not going to succeed on the fourth, and the restart is automatic whether or not you're ready for it.

Keep that count on a volume with the task itself. In memory it resets on exactly the event you're trying to count.

Make the loop legible in logs

One line per iteration: step number, tool name, duration, and whether it succeeded. That's enough to see at a glance whether an agent is progressing or circling.

Keep it to one line. Logs are capped and rotate, and an agent that logs full prompts and responses will evict its own history within minutes — see viewing logs for what you actually get.

FAQ

How do I stop an AI agent looping forever?

Give the loop a maximum iteration count, a wall-clock deadline, a per-task token budget, and repeat detection on identical tool calls.

Why does my agent get killed with no error?

Its message history probably grew past the container's memory limit. The limit is hard, so the process is killed rather than slowed.

Isn't a step limit enough on its own?

No. It bounds steps, not time or money — context grows each lap, so late iterations cost far more than early ones.

What's the cheapest way to detect a stuck agent?

The same tool called with the same arguments twice in a row. It catches stuck loops in three steps rather than fifty.

Why does my agent restart and repeat the same failing task?

The task is persisted but the attempt count isn't. Store attempts with the task and give up after a few.

Do I still need a provider spend limit?

Yes. It's the only limit that holds when the bug is in your own counting.

Wrapping up

The appeal of an agent is that you don't have to specify the steps. The cost is that you don't get to specify when it stops, so that has to come from outside the model — four small pieces of ordinary code, none of which is interesting to write. Write them anyway, before the first unattended run. An agent that gives up after ten steps and tells you why is a system you can debug; one that quietly loops all night is a bill and a mystery.