Skip to content

Concurrent sessions retry a failing provider independently — no shared backoff or circuit breaker (2.0) #55075

Open

Description

@xingchenmengguichu

Description

Follow-up to #52513 (closed during the 1.x → 2.0 transition, with an invitation to re-open if this still applies to 2.0 — it does). Originally reported against 1.18.x; I've now verified at the code level that 2.0.28 still has no shared per-provider retry/health state: each session runs its own retry policy against a failing provider, so N concurrent sessions keep hitting the same failing endpoint at overlapping times, and every retry publishes session-scoped events to all connected clients.

What changed in 2.0 (all refs at tag v2.0.28, packages/core/src/session/runner/):

  • the per-session policy grew: exponential 2s capped at 10s per gap, up to 10 retries per cycle — roughly 84s of waiting per exhausted cycle (67–101s with jitter): retry.ts#L45
  • it is still scoped to a single session: policy(sessionID) closes over its own attempt counters (retry.ts#L64), and the runner instantiates it per step run with SessionRunnerRetry.make(bus, sessionID) (llm.ts#L209)
  • Retry-After is honored per session too, capped at 15 min (retry.ts#L32)
  • there is no provider-level shared state anywhere (no health tracking, no breaker) — so nothing coordinates concurrent sessions against the same failing provider

To be fair, the per-session side improved nicely in 2.0: bounded retries, Retry-After honored with a sane cap. The missing piece is still coordination between sessions.

Net effect on 2.0: during a provider outage/limit window, 5 concurrent sessions produce ~50 futile attempts spread over ~1.5 minutes per exhausted cycle, repeating as long as the provider is down and parents re-dispatch — the same storm as 1.x, just longer.

Behavioral evidence from 1.18.x (mechanism unchanged in 2.0): 2,610 stream error entries over ~7 weeks of heavy parallel use, worst single day 864, one observed minute with 5+ concurrent sessions failing every 2–5 s against the same provider.

Suggested direction — unchanged from #52513: shared state keyed by provider (+ endpoint/credential) in front of the per-session policy:

  • 429: keep honoring Retry-After (2.0 already caps it) but share the resulting next-allowed time across sessions
  • 5xx / connection errors: circuit breaker (consecutiveFailures, closed/open/half-open, one half-open probe), opening after ~one exhausted policy (~84s on 2.0)
  • while open: one deduplicated provider.status event instead of per-session retry events; in-flight streams finish naturally, only new attempts are gated
  • first pass: share state within one server process

Happy to attempt a PR — a small provider-health module next to packages/core/src/session/runner/ looks like a natural home, but I'd love a pointer to where you'd want it.

Plugins

None

OpenCode version

2.0.28

Steps to reproduce

  1. Configure a provider that returns 429/503 (or point it at a mock server that does).
  2. Start one parent session and 3–5 parallel subagents using that provider.
  3. Watch provider traffic and client events while the provider is failing.

Expected: a shared backoff/circuit opens; only one probe request in flight; one throttled provider status event.
Actual: each session retries on its own schedule (up to 10 attempts, ~84s per cycle on 2.0.28); attempts overlap; clients receive per-session retry events from every session.

Operating System

Windows 11 (build 26200)

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for following up on #52513 with the 2.0 code references, and for the clear proposal.

    To restate the request: in 2.0, retry state is kept per session (SessionRunnerRetry.policy(sessionID)), so concurrent sessions and subagents retry the same failing provider independently, and each retry produces its own events. You're asking for retry state shared per provider in front of that policy:

    • a shared next-allowed time for Retry-After on 429s
    • a circuit breaker with a single half-open probe for 5xx and connection errors
    • one deduplicated provider status event in place of per-session retry events

    I've marked this as a feature request, and maintainers will consider it, including where a provider-health module should live if you'd like to open a PR.

    A plugin could only cover part of this today. The session.retry hook carries model.providerID and can change the retry delay, so a plugin could share a per-provider "next allowed" time across retries. It can't hold back a session's first request to a provider that is already failing, combine the per-session retry events, or limit traffic to one test request.

    Related issues:

  2. xingchenmengguichu commented on Oct 13, 2026

    @xingchenmengguichu
    Author

    Thanks for the confirmation.

    I'd like to attempt the PR. Two questions to shape it:

    1. Where should a provider-health module live — alongside packages/core/src/session/runner/, or lower at the provider layer?
    2. Should the gate block a session's first attempt to a failing provider (fail fast with a clear provider unavailable, retrying in Ns error), or only inside the existing retry policy?

    Happy to work with whatever answers you have.

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

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions