Skip to content

Rate limits vs. exhausted credits

This is the distinction claudeloop exists to get right. Both look identical from the outside — an HTTP 429 — but only one of them will ever resolve by waiting.

The two kinds of "no"

WindowExhausted CreditsExhausted
What it means A five-hour / seven-day / seven-day-Opus / seven-day-Sonnet / overage window is temporarily used up The account has no usage credits left
Resolves by waiting? Yes — once resets_at passes No — never, on its own
Resolves by The clock A human buying more credits
Carries a resets_at? Usually Never — the type has no such field
claudeloop's response Probes near the reset time (with a fallback interval so it doesn't trust a far-future timestamp blindly) Probes on a bounded backoff and notifies you that action is needed

A real example of the second case, captured during development:

"apiErrorStatus": 429, "error": "rate_limit",
"errorDetails": "... \"error_code\":\"credits_required\",
  \"disabled_reason\":\"out_of_credits\", \"can_user_purchase_credits\":true"

No reset time appears anywhere in that payload, because none exists. A tool that sleeps a fixed hour and retries — which is exactly what the legacy script this project replaces did — will do that forever.

How classification works

domain/classify.py's classify() function reads three independent signals from a turn — the SDK's typed RateLimitEvent, the result's api_error_status, and the assistant message's error field — specifically because RateLimitEvent is reportedly dropped on some code paths, and a single point of failure in the one function that decides "should I wait or give up" is not acceptable. See ../architecture/domain-model.md#classifypy-turnsignals-capacitystate for the exact precedence rules, and ADR 0003 for why credit signals outrank a stray reset timestamp if both happen to be present.

Handling a credit top-up mid-wait

If claudeloop is waiting on CreditsExhausted and you add credits to your account, it notices on the next scheduled probe — not at some fixed deadline, because there isn't one to wait for. The probe cadence (--credits-probe-interval, default 120s, backing off to --credits-probe-ceiling, default 600s) is what bounds how long it takes to notice; see ADR 0004 for why this is a scheduled probe loop rather than a single sleep, and ../architecture/run-loop-state-machine.md for the exact state sequence, which is directly covered by a test.

What the probe itself costs

The throwaway turn claudeloop sends to re-check capacity is deliberately minimal — one token, no tools, no CLAUDE.md loaded, and configured not to persist a transcript — so it costs nothing meaningful and doesn't pollute your session history with "OK" turns. A rejected probe isn't billed by the API either way.