A quota error looks like a retry, so 15 jobs idled six hours unnoticed
One developer's LaunchAgent fleet hit Claude Code's weekly limit and kept firing. His fix is a shared circuit breaker with a state file, not better error handling inside each job.
The Engineer · Build desk
What happened
- Fifteen scheduled jobs on one developer's Mac hit Claude Code's weekly usage limit and spent six hours returning errors while appearing to run.
- Scripts exit 1 on that error, launchd files it as a failed job, and then starts the same job again on its next cycle.
- launchd sends no failure notice by default, and a plist has no way to say stop running until the quota resets.
- The fix routes every job through a wrapper script via the CLAUDE variable, which runs the real binary and scans what comes back.
- On a quota match the wrapper opens the circuit for six hours, records it in a JSON state file, and fires an alert to Discord.
Why it matters
- capability A locked state file makes the quota a fleet-level fact: the first job to hit the ceiling stops the other fourteen without any of them spending an attempt to find out.
- constraint With no meaningful exit code to key on, the breaker has to match the vendor's error wording, so whoever edits that string decides whether the fleet stops.
- exposure Two of the seven trigger patterns are loose enough that unrelated output mentioning a reset time can park every Claude job on the machine.
- decision Skipping with exit 0 keeps launchd's log clean, which moves the only evidence of a stoppage out of the scheduler and into the notifier.
The arithmetic in the writeup is on the low side. The author prices the outage at 6 hours x 15 jobs = 90 jobs' worth of work [2], which only holds if each job runs once an hour [17]. He also says these LaunchAgent entries fire every 5 minutes or every hour [7]. A five-minute job gets 72 attempts inside a six-hour window [15], so fifteen of them on that cadence would produce 1,080 invocations with no chance of succeeding [16]. Each one forked a process, made the call, was told the weekly limit was reached [3], exited 1, and was rescheduled by launchd [4].
Per-job error handling cannot close that loop, and not because the handling is sloppy. Each invocation is a short-lived process that knows nothing about the run before it or about its fourteen siblings, while the quota is a property of the account rather than of the job. The guard's answer is the cheapest shared memory available: one JSON file, an fcntl.LOCK_EX lock, and os.replace() to swap it in so concurrent writers cannot tear it [10]. That is the entire difference between fifteen jobs each finding the wall and one job finding it on behalf of the rest.
What the design trades away is detection quality. There is no exit code to key on, because Claude Code returns 0, 1, or other values depending on the situation [18], and the error text goes to stderr in a /tmp log nobody tails [6]. So the guard reads prose: seven case-insensitive regexes over the last 131,072 bytes of combined stdout and stderr [9]. Two of them, "rate limit" and "resets? (at|in|on|tomorrow)", will match output that has nothing to do with quota [9], and the penalty for a wrong match is six hours of stopped automation [8] until someone runs --reset or open_until expires [11]. The 128KB tail fails in the opposite direction: a chatty run that pushes the limit message out of the window passes as healthy.
Then there is the window itself. The limit being defended is weekly [3], while the circuit opens for 21,600 seconds and then closes on its own [8][11]. A seven-day limit period has room for twenty-eight of those windows [14], so if the reset has not arrived, the fifteen jobs wake up, trip the breaker again, and keep cycling, at a cost of one real invocation per job per reopening. That is much better than 1,080 blind attempts, but it is a rate limiter on your own retries, not a stop, and the state file never learns when the quota actually returns.
One caveat on sourcing: this is a single developer's account of his own machine, published on dev.to, and the Y1.2M/month attached to the stack is his stated revenue, not a measured loss [1]. The mechanism travels regardless. A scheduler that treats quota exhaustion as a transient error will retry at whatever cadence you configured, for as long as the quota lasts, and will file every attempt as an ordinary failure.
What to watch
- Whether Anthropic changes the wording of its limit errors, since seven regexes are the only trigger the guard has.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence34
- Adoption8
- Hype gap+26
- Incentives62
- Confidence42
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The author's 15 LaunchAgent jobs, behind an autonomous stack he says earns Y1.2M/month, hit Claude Code's weekly quota ceiling and then spent six hours swallowing errors while appearing to work; he discovered it the next morning by checking by hand.
- [2]
Fifteen Claude-invoking jobs run in parallel in the author's environment, so hitting the limit skips all of them at once; he counts the loss as 6 hours x 15 jobs = 90 jobs' worth of work.
- [3]
Claude Code has a weekly usage limit; when it is hit, API calls come back with an error message containing the text "weekly usage limit".
- [4]
Automation scripts running under LaunchAgent mostly emit exit 1 and terminate when they receive that error; launchd records exit 1 as a failed job but runs it again on the next cycle as if nothing happened.
- [5]
macOS launchd does not notify the operator by default when a job fails; ThrottleInterval controls the retry interval, but a plist cannot express "hit the quota, do not run at all until the next limit reset".
- [6]
The quota error message goes to stderr; LaunchAgent dumps stdout and stderr into logs under /tmp, which almost nobody watches continuously.
- [7]
The automation scripts run every 5 minutes or every hour under LaunchAgent.
- [8]
On a quota match the guard sets open_until = now + 21600s (6 hours) with reason "quota-message", writes the state atomically to ~/.claude/state/claude-quota-circuit.json, and sends an immediate notification via ~/.discord/notify.sh to an "alerts" channel.
- [9]
claude-quota-guard.py treats seven regex patterns as a quota hit, including "rate limit" and "resets? (at|in|on|tomorrow)", matched case-insensitively against the last 131,072 bytes (128KB) of Claude's combined stdout and stderr.
- [10]
Circuit state updates take an fcntl.LOCK_EX file lock first and then swap the file in atomically with os.replace(), so the JSON file is not corrupted when multiple jobs read and write it simultaneously.
- [11]
Recovery is either manual, by running claude-quota-guard.py --reset after the limit resets, or automatic, with the circuit returning to CLOSED once open_until has passed (normalize_expired).
- [12]
While the circuit is OPEN the guard blocks direct invocations immediately with exit 75, and skips with exit 0 when called in --job mode.
- [13]
Jobs reach the guard by setting CLAUDE=~/.claude/scripts/claude-quota-guard.py; in the CLOSED state the guard runs the real binary at ~/.local/bin/claude and scans its output.
- [14]
A seven-day limit period contains 28 consecutive six-hour circuit windows, so a breaker that auto-closes after six hours can be reopened up to 28 times before a weekly quota resets.
- [15]
A job scheduled every 5 minutes makes 72 attempts during a six-hour outage.
- [16]
Fifteen jobs all on the five-minute cadence would make 1,080 invocations during a six-hour quota outage, none of which could succeed.
- [17]
The author's figure of 90 lost jobs implies one scheduled run per job per hour, the slower of the two cadences he describes.
- [18]
Claude Code's own exit code varies between 0, 1 and other values depending on the situation, so checking the exit code is not a reliable test.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.to6 Hours, 15 Jobs, Zero Alerts: A Circuit Breaker for Claude Code's Weekly Quota
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.