●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
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.
The price of one item per invocation was a 9.3x startup tax
This shape is not free. What used to be one process handling the whole batch becomes one process per item.
In the same rig I measured 200 items processed by a single process against 200 items processed one per process.
Execution shape
Wall time for 200 items
Per-item delta
One process, 200 items
656 ms
—
200 processes, one item each
6,121 ms
+27.3 ms
That is 9.3x. The extra time is interpreter startup and initialization.
When a stage waits seconds on a model call, 27.3 ms disappears entirely. My overnight job showed no perceptible difference.
Some setups are not so forgiving. If every start re-reads a large index, relaunches an MCP server, or re-acquires credentials, initialization creeps toward the cost of generating an item, and one-item invocations stop paying for themselves.
In that case, do not pin PER_RUN to 1 — use k. Choosing k is simple: pick a value that guarantees each invocation ends immediately after a commit. Then set the cap to "worst case for k items plus one item of headroom."
# Process k items per invocation; cap = k x worst-case + one item of headroomPER_RUN=4WORST_PER_ITEM=0.35 # highest per-item cost over the last 30 days -- not the averageCAP=$(python3 -c "print(round($PER_RUN * $WORST_PER_ITEM + $WORST_PER_ITEM, 2))")PER_RUN=$PER_RUN claude -p "$PROMPT" --max-budget-usd "$CAP"
Using the maximum rather than the average for WORST_PER_ITEM is the part that matters. With an average, the runs that hit a heavy input land the cap in the middle of generation — and the non-monotonic waste comes straight back.
The moment I ran them in parallel, the index stopped being the single source of truth
Having split the work into single items, running them concurrently should have recovered the startup tax. I launched 24 at once.
Entries vanished from the index.
How completion is recorded
Lost records out of 120
Exceptions raised
Fixed temp filename + read-modify-write
92
40
Per-process temp filename + read-modify-write
97
0
Writes serialized with flock
0
0
One marker file per item
0
0
Across 24 workers times 5 trials — 120 items — 97 records fell out. Every artifact file was written correctly. Only the index lost them.
The cause sits outside atomic replacement. os.replace makes the swap atomic, but it does not protect the full read-modify-write sequence. Twenty-four processes read the same stale index, each appends its own single entry, and only the last writer survives.
The first row is a worse failure. With the temp file pinned to .part, one process would have its temp file taken by another before it could call os.replace, producing 40 exceptions — and some trials read an empty index. I believed I was writing atomically while concurrency quietly invalidated the premise of atomicity.
What makes the second row frightening is the zero in the exceptions column. Nothing lands in the log. You find out the next morning when the counts do not add up. The same quality of silence as the truncated text I opened at the start of this piece.
Two fixes worked, and both produced zero lost records.
Serializing writes with flock keeps the existing index structure intact. It cost 453 ms on average at 24 workers — roughly twice the marker approach below. The difference is time spent waiting on the lock.
The other fix drops the single index in favor of one marker file per item. Writes never collide, so no lock is needed. Same 24 workers: 231 ms.
def commit(root, name, artifact_bytes): """One marker file per item. Writes never collide, so no lock is required.""" done = root / "done" done.mkdir(exist_ok=True) p = done / f"{name}.json" t = p.with_name(p.name + f".{os.getpid()}.part") # temp name must be per-process with open(t, "w") as f: json.dump({"name": name, "bytes": artifact_bytes}, f) f.flush(); os.fsync(f.fileno()) os.replace(t, p)def completed(root): """On resume, gather the markers into a completion set.""" d = root / "done" if not d.is_dir(): return set() return {json.loads(p.read_text())["name"] for p in d.glob("*.json")}
If you still want a single index file, rebuild it from the markers. Rebuilding 1,000 entries took 32.1 ms — cheap enough to run once after the parallel phase finishes.
Making temp filenames per-process was required either way. The atomicity of os.replace assumes the source you are swapping in belongs only to you. Sequential runs satisfy that assumption for free, which is why it breaks the instant you go parallel.
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.
Are temp filenames per-process? If concurrency is on the table, a fixed .part name breaks the premise atomic replacement rests on.
Does completion recording survive concurrency? No read-modify-write against a single index. Go with flock or one marker file per item.
Of those nine, I had two — items 1 and 3 — before I started. The rest only became visible once I enumerated the stop points. Items 8 and 9 never surface at all as long as you stay sequential.
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.