Module 5 — Budget and Control: Cost, Time, and Context
6. Stop conditions and spend limits
Description
By the end of this lesson you will be able to define, before delegating any task to an agent, three hard limits — number of iterations, wall-clock time, and dollar spend — that enforce themselves, without you depending on your own judgment at the moment you already have half an hour invested. And you will be able to recognize, while a session is still running, the three signs that it entered an unproductive loop, so you can decide with judgment — not with the hope that the next turn will be the one — whether it is worth narrowing the scope, switching models, redoing the specification, or simply finishing the task yourself.
This matters because the scenario that ruins an afternoon is not the task that fails fast and visibly — you notice that one right away. It is the one that looks about to resolve itself, turn after turn: the agent found "almost" the cause, made "almost" the right fix, and every single turn seems reasonable to keep paying for. An engineer who reviews the month's spend and finds a single debugging session that consumed $18 and forty-five minutes without producing a usable fix does not have a rare case: they have the normal result of not having set a number before starting.
Connection to the module: the previous lesson was about the hidden cost of opening several fronts of work in parallel — review multiplies too. This lesson is about the other extreme: when to cut off a task, alone or in parallel, before it keeps consuming budget without getting closer to a result. An important vocabulary clarification, so this lesson does not get confused with something else you will find in other guides in the ecosystem: the cutoff this lesson talks about is a budget cutoff — turns, time, dollars — not a trust cutoff on a result the agent already produced. If what you need is to decide whether a finished diff gets corrected or discarded for low quality, you already answered that question in lesson 7 of Module 4. Here the task is still running, and often there is not even a complete diff yet to review.
The plumber who sets the cap before opening the wall
You call a plumber about a leak that is not visible on the surface. A professional with a real trade tells you something before touching the first wall: "I'll check for two hours; if in two hours I haven't found the source, I'll stop, tell you what I ruled out, and we decide the next step together, instead of continuing to open walls blindly." That number — two hours — is not decided an hour and a half in, looking at how much wall has already been broken and feeling like "it's close". It is decided before any wall is broken at all, when there is not yet any pressure to keep going "just a little more".
The reason that number gets set before, not during, is not an organizational quirk: it is that during the task there is a real bias that always pushes in the same direction. Once you already have an hour and a half of work invested, stopping feels like admitting that hour and a half was wasted, and that feeling alone — not a real calculation of how much is left — is enough to keep you breaking wall. A limit set in advance does not need you to have, in the moment, the clarity to resist that bias: the number is already decided, and all that is left is to honor it.
The same thing happens with a coding agent, with one practical difference in your favor: the task is easy to quantify in three distinct units, and each one detects a different kind of session that got out of control.
| Limit | What it controls | How it is defined in Claude Code | What happens when it is hit |
|---|---|---|---|
| Iterations (turns) | How many rounds of back-and-forth from the agent you allow before stopping | --max-turns N (print mode only, claude -p) | The process ends on its own, with a nonzero exit code — it does not keep iterating hoping the next turn will be the one |
| Spend | How many API dollars you allow that task to consume | --max-budget-usd N.NN (print mode only) | The process ends before exceeding the amount, with the same kind of error exit |
| Wall-clock time | How much real time you allow it to run, regardless of how many turns or dollars have been consumed | There is no native flag for this — you wrap the call with the Unix timeout command, or time it by hand in an interactive session | timeout kills the process once the deadline is reached, with the standard exit code 124 |
Notice that the three right-hand columns are independent of each other. A task can exhaust the ten allowed turns without spending even half the dollar budget, if each turn is cheap but the agent needs a lot of back-and-forth. And a task can run out of time long before exhausting turns or dollars, if each individual turn is cheap in tokens but takes minutes of real time — for example, because it runs a slow test suite between each attempt. That is why the time limit is not redundant with the other two: it catches a type of runaway session the other two do not see coming.
Worked example
The nightly job inventory_sync.py, which syncs inventory with an external supplier, fails with a generic timeout roughly 1 out of every 15 runs. There is no visible pattern in the logs about when it happens. Before delegating the diagnosis, you define the three limits — not while the session runs, now, before writing the prompt:
- Turns: 10. If in ten rounds of back-and-forth the agent has not isolated the cause, something about the approach is not working and ten more turns are not going to solve it on their own.
- Spend: $4.00. Enough for a real investigation reading several files and logs, not so loose that you would not notice if the session went off the rails.
- Wall-clock time: 25 minutes. A generous value for an ambiguous-cause diagnostic task, but finite.
Command:
timeout 25m claude -p --max-turns 10 --max-budget-usd 4.00 \
"The job src/jobs/inventory_sync.py fails with a timeout \
roughly 1 out of every 15 runs. Investigate the cause \
by reviewing the file and the logs in logs/inventory_sync/, \
and propose a fix. Do not touch any other job." \
> session.log
echo "Exit code: $?"
What to expect, depending on which of the three limits gets hit first:
- If the agent solves the task before hitting any of the three numbers,
claudeends normally,session.loghas the result, and the exit code is0. That is the case where the limits never activated because they were not needed. - If the ten turns or the $4.00 run out before 25 minutes pass,
claudeitself ends the process on its own: a nonzero exit code, and an indication that the turn or budget limit was reached, not an error in the task itself. The limit did exactly what you asked it to do. - If 25 minutes pass without either the turns or the budget running out — for example, because each turn includes running the full job against a test environment and that takes several minutes per attempt —, it is
timeoutthat kills the process from the outside, and$?is going to show124: the standard codetimeoutuses to signal that it was the one that cut things off, not the task ending on its own.
All three outcomes are legitimate ways for the session to end. None of them means "the agent failed" in the sense of having done something wrong — it means the limit you defined before starting did its job.
If you work in an interactive session and not in print mode: neither flag applies — both are exclusive to claude -p. The way to impose the same three limits in a normal conversation is manual: you decide the number before starting ("no more than ten turns with this same approach, no more than $4, no more than 25 minutes"), set a timer, and check /usage — the same tool from lesson 2 — every so many turns to know where you stand. The limit exists just the same; what changes is that nobody enforces it for you, you have to enforce it yourself, and that detail is exactly this lesson's first common mistake.
The three signs a session entered an unproductive loop
The hard limits from the previous section are the backstop: eventually they cut things off, no matter what. But waiting for the full number to be hit — ten turns, $4, 25 minutes — before acting leaves out information that was already available earlier. While the session runs, there are three signs indicating the current approach is not going to converge, well before the numeric limit triggers.
The same fix repeats twice. The agent applies a change, the failure persists, and in a later turn it applies — in different words, in a different file, in a different shape — essentially the same change it had already made. This is the sign that it is not incorporating the evidence from the previous attempt: it did not notice it had already tried this, or did not understand why it did not work.
The diff grows and the problem does not move. Every turn touches more lines and more files, but the real indicator of progress — how many tests fail, whether the original symptom still reproduces — stays exactly the same. A diff that goes from 20 to 200 lines without the failure count changing is not "more work done": it is more surface touched without getting closer to the result.
It changes strategy without resolving any of them. The agent abandons an approach halfway and starts a different one, without having taken the first one to a verified conclusion. Jumping from "the problem is retry handling" to "the problem is the connection pool" to "the problem is the load balancer configuration", all within the same session and without any of the three having been confirmed or ruled out with evidence, is showing motion without showing progress.
Let us follow the inventory_sync.py case to see what all three look like together, with concrete evidence and not just a feeling:
- Turn 2: the agent adds retries with exponential backoff around the HTTP call to the supplier, in
sync_inventory(). It runs the job 15 times against a simulated environment: it fails once, the same rate as before the change. - Turn 4: still failing. The agent adds the same retry mechanism — practically identical, with a different variable name — inside
fetch_supplier_catalog(), a different function that calls the same API. There is no sign in the diagnosis that it noticed the first attempt already covered that code. (Sign 1: same fix, twice.) - Turn 6: still failing 1 out of 15. The agent abandons the retry approach without having ruled it out with evidence — it never got around to confirming whether the timeout happens before or after the retry kicks in — and decides the real problem is the database connection pool. It starts rewriting the entire
ConnectionPoolclass. (Sign 3: strategy change without resolving the previous one.) - Turn 8: the diff went from 20 lines in one file to 210 lines across six files. The job still fails 1 out of 15 runs, the same rate as turn 2. (Sign 2: the diff grew tenfold, the real progress indicator did not move at all.)
None of the three signs, on its own, proves the agent "is bad" in a general sense — in fact, in lesson 4 of this module you saw that even the most capable model produces dead ends on ambiguous-cause tasks. What all three together prove is that this specific attempt, with this specific approach, stopped converging, and giving the same path more turns is not continuing the work: it is continuing to pay for the same blind search. It is the same decision trap you already saw in lesson 7 of Module 4 — each individual step feels reasonable, the sum does not —, but one turn earlier: there you were evaluating an already-finished diff with documented findings; here you are looking at a session that is still running, before any complete diff exists to review.
What to do when the cutoff triggers
Whether the cutoff was triggered by a hard limit (turns, budget, or time ran out) or you triggered it yourself by recognizing one of the three signs before the number was hit, the question is the same: what do you do with the cutoff session? Four exits, and none is automatically the right one — it depends on which sign you saw.
- Narrow the scope. If the task you gave the agent was actually two tasks stacked on top of each other — "find the cause and fix the job" when the diagnosis alone is not even confirmed yet —, cut it into a smaller task: just the diagnosis, verifiable on its own, before asking for any fix. This is the natural exit when you saw sign 1 — same fix repeated —: it usually indicates the agent is missing a specific piece of data (a file, a log, a constraint) that a more narrowly scoped task, with that data made explicit in the prompt, can actually resolve.
- Switch models. If you already narrowed the scope and the problem persists, and what you saw was sign 3 — strategy change without resolving any of them —, that is the sign the task exceeds what the current model tier can sustain at this level of ambiguity. This module's lesson 4 escalation criteria applies here too: if you are already two failed attempts in with the same model on the same approach, escalate, instead of giving it a third attempt at the same tier hoping for a different result.
- Redo the specification. If what you saw was sign 2 — the diff grows, the real indicator does not move —, often the problem is not execution: it is that the original task was missing the constraint needed to narrow the search. In the
inventory_sync.pycase, for example, the prompt never mentioned that the external supplier has a request rate limit that kicks in under certain load conditions — a fact that, written explicitly into the task, would have saved the agent turns 2 through 8 entirely. Going back to the specification and adding that constraint, with the tools from Module 2, is usually worth more than another attempt without that data. - Do it by hand. When the cutoff has already been hit and none of the three above clearly applies, or when the turns that did run already gave you, as a byproduct, enough information to finish the task yourself faster than it would take to restart a new session. In the example, after turn 8 you already know it is not retry handling and not the connection pool — that has already been ruled out with real evidence, even at a high cost —, so finishing the diagnosis with that information in hand, instead of opening another session from scratch, can be the cheapest of the four options.
As a quick reference, here is how the three signs relate to the most likely action:
| What you saw | What it usually means | Typical action |
|---|---|---|
| Same fix, twice | The agent is missing a specific piece of context | Narrow the scope, include that data explicitly |
| The diff grows, the indicator does not move | The symptom was not the cause; the specification is missing a constraint | Redo the specification with the real case that was missing |
| Changes strategy without resolving any | The ambiguity exceeds what this model tier can sustain | Switch models (escalate) |
| The hard limit was hit and no sign was clear | You already have enough diagnosis from the attempts that did run | Finish it by hand with what you already learned |
Common mistakes
Setting the number and then negotiating it with yourself in the moment (conceptual). You defined ten turns before starting, but when you reach turn ten the agent "is close" — it found something, proposed a change, just needs the tests run one more time — and you decide to give it two more turns. Why it happens: all the value of setting the limit before starting comes from there being no pressure to stretch it at that moment; by turn ten that pressure already exists, and it is exactly the same pressure that makes a limit decided in the heat of the moment worthless. How to spot it: if you catch yourself thinking "two more turns and I've got it" after having defined a different number before starting, you are already negotiating the limit instead of honoring it. How to fix it: treat the number set beforehand as a decision already made, not a suggestion — if two more turns really are worth it, that is new information for the next session with a new limit, not an exception for this one.
Setting limits so loose they never trigger (practical). You configure --max-turns 50 and --max-budget-usd 100 "just in case", and in practice the limit never activates because no real session ever goes that far without you aborting it first for other reasons. Why it happens: a generous number feels safer than a tight one, because it reduces the risk of cutting off a legitimate task too soon. How to spot it: review your session history — if no limit ever triggered, you do not have a limit, you have decorative formality. How to fix it: calibrate the number with the cost-by-task-type table you built in lesson 2 — if a task of that type typically takes 4 turns and costs $0.30, a limit of 8 turns and $1.00 is still generous and, at the same time, does trigger when something genuinely went wrong.
Treating every cutoff as if the whole session had to be discarded (practical). The limit was hit, and the automatic reaction is to close everything and start from zero without using anything the cutoff session already produced. Why it happens: a limit that triggers with an error exit code feels like total failure, and the natural reaction is to treat it as one. How to spot it: if, when restarting a task, you give the agent exactly the same prompt as the previous time, without mentioning anything the cutoff session already ruled out, you are paying again for information you already had. How to fix it: before restarting, review what the cutoff session ruled out with real evidence — in the example, "it's not retry handling, it's not the connection pool" — and start the next session with that information already included in the prompt, instead of with a blank page.
Exercises
Exercise 1. An agent investigates why POST /api/webhooks/stripe processes the same webhook twice when Stripe retries delivery. Here is the session log:
- Turn 1: adds a
processed_webhook_idstable and an idempotency check before processing, inhandle_webhook(). Runs the integration tests: 1 out of 4 still fails — the case where two retries arrive almost simultaneously. - Turn 3: adds the same idempotency check, this time inside
webhook_middleware(), without mentioning one already exists inhandle_webhook(). The same case still fails. - Turn 5: decides the problem is the load balancer and starts proposing infrastructure configuration changes outside the application code.
- Turn 7: the diff went from 15 to 90 lines across four files. The same simultaneous-retries case still fails.
Identify at which turn each of the three unproductive-loop signs appears, and which action from this lesson's table you would apply before reaching turn 7.
See solution
Turn 3: sign 1, same fix repeated (the same idempotency check, in a different file, without recognizing it already existed). Turn 5: sign 3, strategy change without resolving the previous one (jumps from application code to infrastructure, without having confirmed or ruled out with evidence whether idempotency alone was enough). Turn 7: sign 2, the diff grew sixfold and the same specific case still fails.
The tightest action appears already at turn 3, not turn 7: sign 1 points to the agent missing a piece of context, not to the approach being wrong. The case that keeps failing — two nearly simultaneous retries — is the clue: a "check if it exists, if not, insert" check has a race window if two requests arrive almost at the same time, unless the table has a database-level unique constraint that rejects the second insert. Narrowing the scope to a specific task — "add a unique constraint on processed_webhook_ids.event_id and handle that constraint violation error as a silent success" — with that data explicit in the prompt, instead of letting the agent keep searching blindly, is cheaper than waiting until turn 7.
Why it works: sign 1 appears before the other two, and acting the moment it appears — instead of waiting for the hard limit — is exactly the difference between using the signs as an early warning and using them only as an explanation for why the session got cut off.
Exercise 2. You have two tasks to delegate: (C) update a dependency's version in package.json and run the full suite to confirm nothing broke; (D) reduce the p95 response time of the search endpoint without changing the API contract, knowing there is no profiler running in production yet. Propose a reasonable number of turns, wall-clock time, and dollar spend for each, with one line of justification per task.
See solution
Task C: 3 turns, 5 minutes, $0.50. It is a mechanical, closed-criterion task — the suite passes or it does not; if it is not resolved by turn 3, something genuinely went wrong (a version conflict the agent cannot resolve on its own), not a legitimate investigation that needs more time.
Task D: 12 turns, 40 minutes, $6.00. It is an ambiguous-cause task with no prior instrumentation — with no profiler, every hypothesis about where the bottleneck is has to be confirmed by running something —, the same profile as Task B from this module's lesson 4. The limits need to be generous compared to Task C, but they are still finite: if by turn 12 the agent is still testing hypotheses without having isolated even one candidate with evidence (for example, a slow query identified with an EXPLAIN), that is already this lesson's sign 3, and it is worth cutting off and redoing the specification by adding the missing instrumentation, instead of continuing to give turns to a search with no instruments.
Why it works: the number of limits does not come from a fixed table — it comes from where the task falls on the same spectrum of "closed criterion, cheap to detect error" versus "ambiguous cause, slow verification" you already used to choose a model in lesson 4. A mechanical task needs tight limits because ten extra turns is already a sign something is off; an ambiguous task needs generous limits because legitimate exploration takes longer — but generous is not the same as unlimited.
Exercise 3. Write the full command, with timeout, --max-turns, and --max-budget-usd, to delegate this task with the following limits: maximum 5 turns, maximum $1.50, maximum 10 minutes of wall-clock time. The task: "Fix the typo in the error message of validate_email() in src/utils/validators.py, which says 'imput' instead of 'input'."
See solution
timeout 10m claude -p --max-turns 5 --max-budget-usd 1.50 \
"Fix the typo in the error message of validate_email() \
in src/utils/validators.py, which says 'imput' instead of 'input'. \
Do not touch anything else in that file." \
> session.log
echo "Exit code: $?"
Why it works: the three limits are deliberately tight — 5 turns, $1.50, 10 minutes — because the task is as mechanical and closed as Task A from lesson 4: changing one word in a string. If this session ended up exhausting any of the three numbers, that would not be evidence the task needed more budget — it would be the clearest possible sign that something strayed from the expected scope, and it would be worth reviewing session.log before anything else.
Summary and next step
You now have this lesson's two halves working together: the three hard limits — turns, time, spend — that you define before starting and that enforce themselves without depending on your judgment halfway through the task, and the three signs — same fix repeated, a diff that grows without the problem moving, a strategy change without resolving any of them — that let you act before the number is hit, when acting is still cheaper. And you saw that neither replaces the other: the limit is the backstop that cuts things off no matter what, the signs are the early warning so you do not have to reach that point.
Before moving on you should be able to: set the three hard limits for a new task before delegating it, justifying the number with the task's profile and not with an arbitrary figure; recognize at least one of the three unproductive-loop signs by looking at a turn log, even when the numeric limit has not been hit yet; and choose, among this lesson's four exits, the one that matches the sign you saw, instead of always restarting the same way regardless of what happened.
You now know when to cut things off. What is left is bringing together all the numbers you have been logging since lesson 2 — cost, iterations, time, and now also how many sessions ended up cut off before reaching a result — into a single figure: the real cost until there is integrated code, and why that figure almost never matches the feeling of how fast the work felt. That is the next lesson.
Resources
- Manage costs effectively — Claude Code's official reference on
/usageand spend tracking, for the case of interactive sessions where limits are enforced by hand. - CLI reference — Claude Code — full documentation of
--max-turnsand--max-budget-usd, including the clarification that both flags are exclusive to print mode (claude -p). - Building effective agents — Anthropic — Anthropic's engineering guide on agent design, which explicitly names stop conditions (like a maximum number of iterations) as part of the control needed in any agentic system, not just coding tasks.
- timeout invocation — GNU Coreutils Manual — the Unix command that imposes the wall-clock time limit when there is no native flag for it, with the exit code
124signaling that it was time, not the task, that cut the session off.