Skip to content

Honour rate_limit_event.resetsAt in retry backoff #74

Description

@moodmosaic

Summary

When agent_is_retriable classifies an error as rate_limited and the underlying rate_limit_event carries a resetsAt timestamp (and optionally rateLimitType: "five_hour" | "weekly"), sleep until resetsAt + small jitter instead of running the current exponential backoff to its 30 min cap. Keep the existing exponential path for transient errors (overloaded, api_error / 500, network, zero-token exits) and for rate-limited errors that don't carry a usable resetsAt. All waits remain bounded by MAX_RETRY_WAIT.

Touchpoints:

Open questions (default in parens, call them out in the PR):

  1. agent_is_retriable interface: return two tab-separated fields <kind>\t<resetsAt_or_empty> (default), or keep the boolean-ish return and add a sibling agent_retry_hint?
  2. Jitter: small uniform + rand(5..30 s) on top of resetsAt to desynchronise multi-agent swarms (default yes).
  3. Floor: minimum wait when resetsAt - now <= 0 due to clock skew or already-reset state (default 30 s).
  4. v1 scope: Claude Code only. Gemini/Codex CLI get separate issues once their native payload shapes are investigated.

Backward compatibility: fully preserved. Missing / unparseable resetsAt → today's exponential ramp. Other drivers unchanged. MAX_RETRY_WAIT = 0 still disables retry entirely.

Motivation

See discussion at #73.

Exponential backoff is the right default for unknown-duration failures, but wrong for the 5-hour / weekly rate limit because the API already tells us exactly when the window refreshes. Every rate_limit_event in stream-json carries a field like:

{"type":"rate_limit_event",
 "rate_limit_info":{"status":"rejected",
                    "resetsAt":1775505600,
                    "rateLimitType":"five_hour"}}

(See docs.anthropic.com for the Opus 4.7 rate-limit surface and news.ycombinator.com/item?id=47664442 for the Claude Code team's explanation.) The current retry loop ignores both resetsAt and rateLimitType, which creates two compounding waste modes on a 5-hour limit:

  • Probe waste early in the window. The first six retries happen at 30 s / 60 s / 120 s / 240 s / 480 s / 960 s — all guaranteed-to-fail on a five-hour-to-go limit. Six doomed API calls per agent per event.
  • Oversleep waste late in the window. Once _backoff saturates at 1800 s, a limit that lifts one minute after the agent falls asleep costs 29 minutes of idle agent time, multiplied by the number of agents hitting the wall together (typically all of them on a shared subscription).

Concretely, with a 6-agent swarm hitting the wall mid-window and ramping to the 1800 s cap, worst-case wasted capacity is ~3 agent-hours per rate-limit event. Across a running week this compounds to meaningful real-money loss on Max/Pro subscriptions.

A fixed 15-30 min interval (considered as an alternative) caps oversleep but regresses transient-error handling (a 30 s blip now costs 15-30 min to recover from) and still blind-probes throughout a 5-hour window (10-20 doomed calls). Honouring resetsAt is strictly better than both: one sleep, one retry, no probe waste, oversleep bounded by the jitter.

This is a patch-level, opt-in improvement. If the field is present we use it; if not we keep doing what we do today. Target release: 0.19.3.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions