Skip to content

retry middleware — RESOURCE_EXHAUSTED retries use exponential backoff without consulting Retry-After header #5270

Closed

Description

@SirBrenton

Finding

The retry middleware in js/plugins/middleware/src/retry.ts includes RESOURCE_EXHAUSTED in its default retry statuses but uses pure exponential backoff without consulting the provider’s Retry-After header.

When a provider returns HTTP 429 with Retry-After: 60, the retry middleware fires at initialDelayMs (default 1000ms) intervals — inside the provider cooldown window. All retries fail until the window expires naturally.

Evidence

DEFAULT_RETRY_STATUSES includes RESOURCE_EXHAUSTED.

Retry delay computation:

let delay = currentDelay;
if (!noJitter) {
  delay = delay + 1000 * Math.pow(2, i) * Math.random();
}
await new Promise((resolve) => __setTimeout(resolve, delay));
currentDelay = Math.min(currentDelay * backoffFactor, maxDelayMs);

No Retry-After header consulted at any point.

The correct behavior

When RESOURCE_EXHAUSTED is received, check the response headers for Retry-After. If present, use that value as the minimum delay before retrying. Fall back to exponential backoff only when the header is absent.

Related pattern

retry-after-ignored-exponential-backoff

Corpus reference: https://github.com/SirBrenton/pitstop-truth

Activity

  1. MichaelDoyle commented on May 11, 2026

    @MichaelDoyle
    Contributor

    Hi @SirBrenton - please provide an example of a model provider used by Genkit that uses the Retry-After header.

  2. SirBrenton commented on May 11, 2026

    @SirBrenton
    Author

    @MichaelDoyle — Anthropic’s API returns Retry-After on 429 responses. Example:

    HTTP/1.1 429 Too Many Requests
    retry-after: 60
    anthropic-ratelimit-requests-reset: 2026-05-10T12:00:00Z

    When this fires through the retry middleware with RESOURCE_EXHAUSTED, the middleware retries at 1s intervals for 60 seconds — burning all attempts inside the cooldown window.

    Corpus receipt documenting this behavior:
    PT-2026-03-21-github-openclaw-venice-models-429-retry-after-unwired

    OpenAI also sends Retry-After. Google’s Gemini API sends it as well — documented in the corpus.

  3. MichaelDoyle commented on May 11, 2026

    @MichaelDoyle
    Contributor

    @pavelgj @ifielker @apascal07 this one is a bit vage/general - but does make sense. Do we have a facility for relaying the back-off time via generate today? If not, could be a nice addition. It would vary plugin by plugin; contrary to the report, the various model providers and SDKs each handle this differently.

  4. SirBrenton commented on May 11, 2026

    @SirBrenton
    Author

    Agreed, @MichaelDoyle — it varies by plugin.

    The Google AI plugin is the clearest case: js/plugins/google-genai/src/googleai/client.ts already classifies 429 as RESOURCE_EXHAUSTED. The retry middleware could read error.headers?.get('retry-after') before computing delay.

    Happy to sketch a plugin-specific fix if that’s more useful than a middleware-level change.

  5. pavelgj commented on May 11, 2026

    @pavelgj
    Member

    Totally valid feature request. We would have to plumb the "retry after" info through the GenkitError... add a field in there (I think must be a dedicated field) and if the plugin properly sets it, then middleware can respect it.

    Option 1: top-level retryAfterMs field on GenkitError

    class GenkitError extends Error {
      status: StatusName;
      detail?: any;
      code: number;
      retryAfterMs?: number;  // new
      // ...
    }

    Option 2: convention in existing detail field

    throw new GenkitError({
      status: 'RESOURCE_EXHAUSTED',
      detail: { ...jsonBody, retryAfterMs: 60000 },
    });
    // Middleware reads: error.detail?.retryAfterMs

    Option 3: new responseMetadata (or httpMetadata) bag on GenkitError

    class GenkitError extends Error {
      status: StatusName;
      detail?: any;
      code: number;
      responseMetadata?: {     // new
        retryAfterMs?: number;
        headers?: Record<string, string>;
      };
    }

    I'd choose between 1 and 3...

  6. SirBrenton commented on May 11, 2026

    @SirBrenton
    Author

    @pavelgj — Option 1 feels cleanest. A dedicated retryAfterMs field makes the intent explicit and keeps middleware logic simple.

    Looking forward to seeing it land.

  7. pavelgj commented on May 19, 2026

    @pavelgj
    Member

    See #5343

  8. SirBrenton commented on May 19, 2026

    @SirBrenton
    Author

    @pavelgj this is exactly what the corpus is for.

    When #5343 merges, I'll add a receipt documenting the fix. That becomes the canonical reference for Retry-After propagation in framework middleware.

    Thank you for the architecture discussion.
    Option 1 was the right call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions