Skip to content

Rate limit retries every ~1 second with no backoff or max limit (OpenAI provider) #30510

Closed as not planned

Description

@jychen

Description

Bug

When hitting rate limits (429) with OpenAI providers (e.g., ChatGPT 5.5), opencode retries approximately every second indefinitely, with no effective exponential backoff and no maximum retry limit.

Root Cause

Three compounding issues:

  1. OpenAI SDK internal retries not disabled
    opencode sets maxRetries: input.retries ?? 0 on the AI SDK's streamText() call, which disables retries at the AI SDK layer. However, the underlying OpenAI SDK (openai npm package) has its own separate retry mechanism with DEFAULT_MAX_RETRIES = 2, which is never overridden. This means every call to the OpenAI provider gets retried up to 2 times internally with ~0.5-1s delays before the error even reaches opencode's session retry logic.
    The OpenAI SDK's RetryHelper always retries 429s, creating a hidden retry loop invisible to opencode's backoff calculation.
  2. Off-by-one in backoff delay calculation
    The session retry delay formula in session/retry.ts:
    RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
    // = 2000 * 2^(attempt - 1)
    Effect's Schedule.InputMetadata.attempt is 0-indexed, so on the first retry attempt = 0, yielding:
    2000 * 2^(0-1) = 2000 * 0.5 = 1000ms
    This halves the intended initial delay from 2 seconds to 1 second.
  3. No maximum retry limit
    The session retry policy (retry.ts → policy()) retries indefinitely as long as retryable() returns truthy. For 429 rate limit errors, it always does. The backoff caps at 30 seconds (RETRY_MAX_DELAY_NO_HEADERS) but never stops retrying.

Observed Behavior

With a persistent rate limit from ChatGPT 5.5:

  • OpenAI SDK retries 2x internally (~0.5-1s apart)
  • Error propagates to session retry → waits ~1s (off-by-one)
  • Calls OpenAI SDK again → 2 more internal retries
  • Repeat forever
    This creates the appearance of ~1 second retries with no backoff.

Expected Behavior

  1. OpenAI SDK's internal retries should be disabled (set maxRetries: 0 on the provider SDK) so opencode's own backoff logic has full control
  2. Initial retry delay should be 2 seconds, not 1 second (fix off-by-one: use attempt instead of attempt - 1, or adjust for 0-indexing)
  3. Session retry should have a configurable maximum retry limit (e.g., 5-10 attempts) before giving up and surfacing the error to the user

Workaround

Press Escape / Ctrl+C to abort the session when stuck in a retry loop.

Affected Code

  • packages/opencode/src/session/retry.ts — delay formula and no max limit
  • packages/opencode/src/provider/provider.ts — OpenAI SDK maxRetries not set to 0
  • packages/opencode/src/session/llm.ts — maxRetries: input.retries ?? 0 only affects AI SDK layer

Plugins

No response

OpenCode version

1.15.13

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

Ubuntu 22.04

Terminal

No response

Activity

  1. github-actions commented on Jun 3, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct (e.g., the specific OpenAI SDK internal retry interaction described here), please clarify what makes it different from the above.

  2. Jomy2323 commented on Jul 10, 2026

    @Jomy2323

    I hit this in production-like conditions and got to the exact mechanism.

    Root cause: on quota-class 429s, Azure OpenAI returns both retry-after: 30 and retry-after-ms: 0. delay() in packages/opencode/src/session/retry.ts gives retry-after-ms priority — and "0" is truthy, parseFloat("0") === 0 passes the NaN guard, so it returns a 0ms delay on every attempt. Combined with the session retry policy having no max attempt count (#21960, #17648), the loop runs at network round-trip speed: I measured ~5 sustained requests/second for ~30 minutes (thousands of requests, zero tokens served, no error surfaced).

    Repro: point opencode at a local mock that always answers 429 with both headers above — it hammers at RTT speed. Remove retry-after-ms from the mock and it politely retries every 30s. The diff between those two runs is the bug. (Most mocks only send retry-after, which is why this hides.)

    Deterministic check: calling delay(n, error) with responseHeaders: { "retry-after": "30", "retry-after-ms": "0" } returns 0 for any n.

    Proposed fix (happy to PR): treat non-positive hints as "no usable hint" — add && parsedMs > 0 / && parsedSeconds > 0 so a zero/negative value falls through (ms → seconds → exponential backoff). With Azure's real headers the delay becomes 30s. A separate PR could add a max attempt count to SessionRetry.policy per #21960. Note: one existing test ("policy updates retry status and increments attempts") uses retry-after-ms: "0" as a fixture and implicitly depends on the 0ms behaviour — it would need a small fixture bump.

  3. github-actions commented on Sep 9, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions