●DEADLINE — Today, August 2, is the last day to claim the one-time $100 promotional credit. It expires September 17, so claiming and spending have separate deadlines●GRANT — Applications for the AI for Science grant close today. Researchers working on rare genetic diseases can receive up to $50,000 in Claude credits over six months●FORK — Claude Code now runs /fork as a background session, and the surrounding /resume and /background flows have been tidied up alongside it●GUARD — Safeguards around WebSearch, subagent spawns, and Bash execution have been tightened. Worth checking whether any of your existing automation quietly stops working●COUNTDOWN — Sonnet 5 stays at $2/$10 per Mtok through August 31. Standard pricing of $3/$15 per Mtok starts September 1, so revisit any estimate that spans the change●FABLE — Fable 5 is included in Max and Team Premium up to 50% of weekly usage limits, while Pro and Team Standard now reach it through metered credits●DEADLINE — Today, August 2, is the last day to claim the one-time $100 promotional credit. It expires September 17, so claiming and spending have separate deadlines●GRANT — Applications for the AI for Science grant close today. Researchers working on rare genetic diseases can receive up to $50,000 in Claude credits over six months●FORK — Claude Code now runs /fork as a background session, and the surrounding /resume and /background flows have been tidied up alongside it●GUARD — Safeguards around WebSearch, subagent spawns, and Bash execution have been tightened. Worth checking whether any of your existing automation quietly stops working●COUNTDOWN — Sonnet 5 stays at $2/$10 per Mtok through August 31. Standard pricing of $3/$15 per Mtok starts September 1, so revisit any estimate that spans the change●FABLE — Fable 5 is included in Max and Team Premium up to 50% of weekly usage limits, while Pro and Team Standard now reach it through metered credits
Resuming After a Budget Stop: What 36 Enumerated Stop Points Taught Me
A budget cap that halts a headless run is neither success nor failure. I enumerated every stop point, measured resume integrity and total spend, and ended up changing what the cap applies to.
One morning I opened the output of a job I had left running overnight, and the text stopped mid-sentence. The error log was empty. The exit code matched none of the patterns I had planned for.
The file was there, and it was a reasonable size. So the downstream step decided it had already been produced and moved on.
What had broken was not the job. It was an assumption inside my resume logic.
A budget stop is neither success nor failure
Claude Code's headless mode accepts --max-budget-usd, which terminates a run once cumulative cost crosses the threshold. Like the cap on concurrent subagents (currently 20), it exists to stop runaway work.
The part that is easy to miss is that this termination is a third kind of exit — neither success nor failure.
Exit type
What happened
Does retrying help?
Normal exit
All steps finished
Not needed
Crash or exception
Abnormal termination
Yes, if the cause was transient
Budget cap stop
A healthy step cut short by policy
No — the same cap stops at the same place
Crash-oriented designs assume you can detect an abnormal state and unwind it. A budget stop is the opposite: the process did exactly what policy told it to do. There is no anomaly recorded anywhere to unwind.
And the place it stops is decided by the moment cumulative cost crosses the threshold — that is, a position unrelated to any checkpoint I designed. It might land in the middle of writing a file, or immediately before the index update that would have made the write official.
I wanted to understand that "you don't know where it stops" property with numbers rather than intuition.
A rig for enumerating stop points
Sweeping --max-budget-usd across dozens of real runs would cost real money. For an indie developer's budget, that is not a reasonable way to learn something structural.
So I built a rig with the same shape in a Linux sandbox: a pipeline that charges cost units per step and calls os._exit(9) the instant the threshold is crossed. Exiting without unwinding is precisely what makes it resemble a budget stop.
#!/usr/bin/env python3"""Pipeline used to enumerate stop points. mode=naive : overwrite in place; resume decides by file existence mode=safe : write .part, fsync, os.replace; resume decides by index"""import json, os, sys, pathlibROOT, CAP, MODE = pathlib.Path(sys.argv[1]), float(sys.argv[2]), sys.argv[3]ART, IDX = ROOT / "artifacts", ROOT / "index.json"ITEMS = ["alpha", "bravo", "charlie", "delta"]spent = 0.0def charge(u): """Charge after each completed step; cross the cap and die without unwinding""" global spent spent += u with open(ROOT / "spend.log", "a") as f: f.write(f"{u}\n") if spent > CAP: os._exit(9) # policy stop: not an exception, so no finally runsdef read_index(): return json.loads(IDX.read_text())["items"] if IDX.exists() else []def write_index(items): if MODE == "safe": t = IDX.with_suffix(".json.part") with open(t, "w") as f: json.dump({"items": items}, f) f.flush(); os.fsync(f.fileno()) os.replace(t, IDX) # atomic swap within the same filesystem else: IDX.write_text(json.dumps({"items": items}))ART.mkdir(parents=True, exist_ok=True)if MODE == "safe": for p in ART.glob("*.part"): p.unlink() # sweep leftovers from the previous interruptionitems = read_index()done = {e["name"] for e in items}for name in ITEMS: if MODE == "naive": if (ART / f"{name}.md").exists(): continue # naive rule: the file exists, so it must be done else: if name in done: continue # the index is the single source of truth charge(1.0) # plan body = f"# {name}\n" + "x" * 200_000 + "\nEND\n" charge(2.0) # generate target = ART / f"{name}.md" path = target.with_suffix(".md.part") if MODE == "safe" else target with open(path, "w") as f: f.write(body[: len(body) // 2]) f.flush(); os.fsync(f.fileno()) charge(0.5) # a stop point placed *inside* the write f.write(body[len(body) // 2 :]) f.flush(); os.fsync(f.fileno()) if MODE == "safe": os.replace(path, target) charge(0.5) items.append({"name": name, "bytes": len(body)}) write_index(items) # commit first, charge second — this ordering matters later charge(0.5)print("COMPLETE")
Each item costs 1.0 + 2.0 + 0.5 + 0.5 + 0.5 = 4.5 units, so four items have an ideal total of 18.0. Sweeping the cap from 0.5 to 18.0 in 0.5 increments walks 36 distinct stop points.
The verifier is deliberately strict. Every indexed entry must exist on disk, match its recorded byte count, and end with a sentinel — and no .part file may remain.
#!/usr/bin/env python3"""Strict post-resume verification: size match AND terminator, not just presence"""import json, sys, pathlibROOT = pathlib.Path(sys.argv[1])ART, IDX = ROOT / "artifacts", ROOT / "index.json"EXPECT = ["alpha", "bravo", "charlie", "delta"]bad = []idx = {e["name"]: e["bytes"] for e in (json.loads(IDX.read_text())["items"] if IDX.exists() else [])}for n in EXPECT: p = ART / f"{n}.md" if n not in idx: bad.append(f"missing-index:{n}"); continue if not p.exists(): bad.append(f"missing-file:{n}"); continue data = p.read_bytes() if len(data) != idx[n]: bad.append(f"size-mismatch:{n}({len(data)}!={idx[n]})") elif not data.endswith(b"END\n"): bad.append(f"truncated:{n}")for p in ART.glob("*.part"): bad.append(f"leftover-part:{p.name}")print(("CORRUPT " + ",".join(bad)) if bad else "OK")
One caveat worth stating plainly: these numbers come from the rig, not from Claude Code's actual billing granularity, which is an implementation detail I cannot observe from outside. What transfers is the structure — a stop driven by internal charge points that do not line up with the boundaries you designed. That structure was what I wanted to examine.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A resume that treats file existence as completion silently accepted a 100,006-byte artifact that should have been 200,013 bytes (8 of 36 stop points ended in a corrupt final state)
✦The unbounded retry loop that appears the moment you add atomic writes, and the progress gate that cuts it off after 3.5 cost units instead of 42.0
✦Total spend is not monotonic in the cap. cap=8.0 cost 30.0; cap=8.5 cost 18.0. Scoping the cap to one resumable unit made waste 0% at every cap value
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
alpha.md is 100,006 bytes — exactly half of the expected 200,013 — yet it exists, so the resume skipped it. Because it was skipped, it never reached the index either. A half-written artifact simply stays on disk forever.
That was the truncated text I had found in the morning.
The danger is not the moment of the stop. It is the moment a later run misreads the partial state as complete. Introducing a budget cap means manufacturing that opportunity on a schedule, at positions the designer never chose.
Atomic writes and index-first resume
The remedy is a classic combination.
Write artifacts to .part, fsync, then os.replace into the final name. Within one filesystem the swap is atomic, so a half-written file never appears under the real name.
Sweep leftover .part files at startup so the previous interruption cannot leak into this run.
Base completion on the index, never on file presence. Update the index through the same atomic replace.
Append to the index after the artifact swap succeeds. The reverse order produces entries that are recorded but absent.
With safe mode, all 36 stop points resumed into a healthy final state.
The cost question remained. Extra fsync calls and a rename are not free. I ran a 200-item pipeline five times per mode and averaged.
Mode
Wall time per run (200 items)
Per item
naive
0.532 s
2.66 ms
safe
0.687 s
3.44 ms
That is a 29.1% difference, or 0.78 ms per item. When the real cost of each item is a model call measured in seconds, this vanishes into noise. I found no reason to trade away atomicity.
So far, everything matched expectations. The surprises came next.
Two results that ran against my intuition
Atomicity created a retry loop that burns budget forever
Having made the pipeline safe, wrapping it in "retry until complete" should have finished the job. I set the cap to 3.0 and ran both modes to completion.
Mode
Attempts
Total spend
Final state
naive
5
14.0
"Completed" with a corrupt artifact
safe
12 (loop limit)
42.0
Nothing produced at all
Against an ideal of 18.0, safe spent 42.0 and produced nothing. At cap 3.5 it climbed to 48.0.
The reason is arithmetic. One item needs 4.5 units, and a cap of 3.0 never reaches the first commit. Because an atomic design refuses to commit partial work, it unwinds cleanly and stops in exactly the same place every time. Every retry buys zero progress.
The irony is that the mode which happily ships corrupt output was the cheaper failure.
What was missing was not atomicity but progress observation. Compare committed counts before and after each attempt, and bail the moment the count fails to move.
#!/usr/bin/env bash# Progress-gated retry: bail out before burning budget on a non-advancing attemptset -uMODE=$1; CAP=$2; MAXATT=${3:-12}; D=$4progress () { python3 - "$D" <<'PY'import json, pathlib, sysp = pathlib.Path(sys.argv[1]) / "index.json"print(len(json.loads(p.read_text())["items"]) if p.exists() else 0)PY}ATT=0; STALL=0while [ "$ATT" -lt "$MAXATT" ]; do BEFORE=$(progress); ATT=$((ATT + 1)) python3 pipeline.py "$D" "$CAP" "$MODE" >/dev/null 2>&1 && break AFTER=$(progress) if [ "$AFTER" = "$BEFORE" ]; then STALL=1 # the cap sits below the cost of one unit of work echo "no forward progress after attempt ${ATT}: raise --max-budget-usd" >&2 break fidoneexit "$STALL"
With the gate in place, cap 3.0 bails after a single attempt having spent 3.5 units. 42.0 became 3.5. A budget cap and a progress gate are two halves of one part. Ship only the first half and the safety mechanism turns into a spending mechanism.
Total spend is not monotonic in the cap
With the gate active, I swept the cap from 4.0 to 12.0 in 0.5 steps and recorded total spend to completion. The ideal is 18.0.
Cap
Attempts
Total spend
vs ideal
4.0
5
18.0
±0%
4.5
4
21.0
+16.7%
5.5 – 7.0
4
27.0
+50.0%
8.0
4
30.0
+66.7%
8.5
3
18.0
±0%
9.0 – 9.5
2
19.0
+5.6%
10.0 – 11.5
2
21.0
+16.7%
Raising the cap from 8.0 to 8.5 — a change of 0.5 — dropped total spend from 30.0 to 18.0. A 40% reduction from loosening the budget.
Tracing it explained the shape. This pipeline charges immediately after committing an artifact. At cap 8.0 the threshold is crossed just before the second commit, so a full item's generation cost is discarded, and that loss repeats on every attempt. At cap 8.5 the commit lands first, so nothing is thrown away.
Waste, in other words, is determined by where the cap falls relative to internal charge points you cannot see from outside. There is no reliable way to know the granularity at which a real model call accrues cost. Which means the whole idea of calculating an optimal cap on paper was mistaken.
Apply the cap to the smallest resumable unit, not to the batch
The answer was not to choose the cap more cleverly. It was to change what the cap applies to.
A cap lands mid-work because one invocation processes the entire batch. Limit each invocation to a single item, and the cap can only ever land on a seam.
PER_RUN = int(os.environ.get("PER_RUN", "0")) # 0 means unlimitedcommitted = 0for name in ITEMS: if PER_RUN and committed >= PER_RUN: break # exit normally after one commit ... write_index(items) committed += 1 charge(0.5)
I re-measured across caps from 4.5 to 12.0.
Cap
Attempts
Total spend
vs ideal
4.5
4
18.0
±0%
6.0
4
18.0
±0%
8.0
4
18.0
±0%
12.0
4
18.0
±0%
Waste is 0% regardless of the cap value. The tuning exercise disappears entirely.
There is a useful side effect. Each invocation is short, so the cap changes meaning: it stops being a runaway brake and becomes a per-item cost ceiling. When one item demands three times its usual cost, you drop that item instead of the whole batch — and you get a signal pointing at the input responsible.
I settled on this shape. Running unattended overnight as an indie developer, having exactly one question to answer in the morning — how far did it get — is worth a great deal.
A checklist before you adopt this
If you are assembling something similar, checking in this order should spare you the holes I fell into.
Are artifact writes atomic? Temp file, fsync, os.replace. No direct writes to the final path left anywhere.
Is completion decided by the index? Not by file presence or size. Presence checks will happily accept a half-written file.
Is the commit-then-record order correct? Append to the index only after the artifact swap succeeds. The reverse creates entries with no file behind them.
Do you sweep partials at startup? Delete leftover .part files before doing any work.
Is there a progress gate? Detect a non-advancing attempt and bail. Without it, the cap becomes a spending mechanism.
Does the cap apply to the smallest resumable unit? One item per invocation, not the whole batch.
Is the exit code interpreted correctly? A budget stop must not fall into a branch that retries transient failures unconditionally. The same cap reproduces the same stop.
Of those seven, I had two — items 1 and 3 — before I started. The rest only became visible once I enumerated the stop points.
When we build something that runs unattended, we naturally think about what to do when it fails. But the halt a budget cap produces is not a failure. It is a quiet seam, placed by correct behavior, somewhere entirely unrelated to the boundaries we drew.
Counting those seams seems to be the only place the work can start. If this saves even one person the morning I had, it will have been worth writing.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.