# How OpenCode Implements Retry Logic for Failed LLM Operations: A Deep Dive into the Dual-Layer Architecture

> Discover how OpenCode implements retry logic for failed LLM operations. Explore its dual-layer architecture featuring exponential back-off and LLM-specific session retries.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: deep-dive
- Published: 2026-02-16

---

**OpenCode handles transient LLM failures through a dual-layer retry system that combines a generic exponential back-off utility with an LLM-specific session retry handler that honors HTTP `Retry-After` headers and supports user-initiated aborts.**

OpenCode, the open-source AI coding assistant from `anomalyco/opencode`, implements sophisticated retry logic for failed LLM operations to ensure reliability when interacting with flaky provider APIs. The architecture separates generic async retry mechanics from domain-specific LLM error handling, allowing both reusable utilities and context-aware recovery strategies. This article examines the implementation across the utility layer and the session processing loop.

## The Dual-Layer Retry Architecture

OpenCode's retry mechanism operates through two distinct layers that work in concert to handle failures gracefully.

The **generic retry helper** located in [`packages/util/src/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/util/src/retry.ts) provides a reusable wrapper for any asynchronous function, implementing exponential back-off and transient error detection. This utility knows nothing about LLMs and can retry file system operations, network requests, or database queries.

The **LLM-specific session retry** module in [`packages/opencode/src/session/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/retry.ts) contains domain logic for determining which LLM errors are recoverable, calculating delays based on provider headers, and performing abort-aware sleeps. This layer integrates directly into the session processor loop that manages the conversation state.

## Generic Retry Utility for Async Operations

The foundation of OpenCode's resilience starts with a generic retry function that wraps any flaky async operation.

### Exponential Back-Off Implementation

The `retry()` function in [`packages/util/src/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/util/src/retry.ts) implements a configurable exponential back-off strategy:

```typescript
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
  const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options

  let lastError: unknown
  for (let attempt = 0; attempt < attempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      if (attempt === attempts - 1 || !retryIf(error)) throw error
      const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay)
      await new Promise((resolve) => setTimeout(resolve, wait))
    }
  }
  throw lastError
}

```

This implementation defaults to **three attempts** with a base delay of **500 milliseconds**, doubling the wait time after each failure while capping the maximum delay at **10 seconds**. Callers can customize every parameter, including providing a custom `retryIf` predicate to determine which errors warrant a retry.

### Transient Error Detection

The default `isTransientError` predicate identifies common network failure patterns such as `"load failed"`, `"econnreset"`, and `"timeout"` by inspecting error messages. This ensures that permanent failures like authentication errors or invalid arguments fail fast while transient network hiccups trigger the back-off sequence.

## LLM-Specific Session Retry Logic

While the generic utility handles mechanical retry logic, the session layer adds semantic understanding of LLM provider errors.

### Determining Retryability with `retryable()`

The `SessionRetry.retryable()` function in [`packages/opencode/src/session/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/retry.ts) filters errors to determine which warrant a retry attempt:

```typescript
export function retryable(error: ReturnType<NamedError["toObject"]>) {
  if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
  if (MessageV2.APIError.isInstance(error)) {
    if (!error.data.isRetryable) return undefined
    if (error.data.responseBody?.includes("FreeUsageLimitError"))
      return `Free usage exceeded, add credits https://opencode.ai/zen`
    return error.data.message.includes("Overloaded")
      ? "Provider is overloaded"
      : error.data.message
  }

  // Attempt to parse a structured JSON error payload …
}

```

This function returns `undefined` for non-retryable errors like **context overflow**, while returning descriptive messages for retryable `APIError` instances that include flags like `isRetryable: true` or specific provider messages such as "Overloaded".

### Honoring HTTP Retry-After Headers

When an LLM provider returns a 429 status or includes rate-limit headers, OpenCode's `SessionRetry.delay()` function prioritizes the provider's guidance over calculated back-off. It parses `retry-after-ms` (milliseconds), `retry-after` (seconds or HTTP-date format), and uses those exact values before falling back to exponential back-off:

```typescript
export function delay(attempt: number, error?: MessageV2.APIError) {
  if (error) {
    const headers = error.data.responseHeaders
    if (headers) {
      const retryAfterMs = headers["retry-after-ms"]
      if (retryAfterMs) {
        const parsedMs = Number.parseFloat(retryAfterMs)
        if (!Number.isNaN(parsedMs)) return parsedMs
      }

      const retryAfter = headers["retry-after"]
      if (retryAfter) {
        const parsedSeconds = Number.parseFloat(retryAfter)
        if (!Number.isNaN(parsedSeconds)) return Math.ceil(parsedSeconds * 1000)

        // HTTP‑date format fallback
        const parsed = Date.parse(retryAfter) - Date.now()
        if (!Number.isNaN(parsed) && parsed > 0) return Math.ceil(parsed)
      }

      // No valid header → exponential back‑off
      return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
    }
  }

  // No error or no headers → capped exponential back‑off
  return Math.min(
    RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1),
    RETRY_MAX_DELAY_NO_HEADERS
  )
}

```

The implementation starts with a **2000 millisecond** base delay, doubling with each attempt (`RETRY_BACKOFF_FACTOR = 2`), while capping the delay at **30 seconds** (`RETRY_MAX_DELAY_NO_HEADERS`) when no headers are present.

### Abort-Aware Sleep Mechanism

To ensure responsiveness, OpenCode implements abort-aware retry logic through the `SessionRetry.sleep()` function. This utility accepts an `AbortSignal` and immediately rejects with a `DOMException` named "AbortError" if the user cancels the session during a retry delay:

```typescript
export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    const abortHandler = () => {
      clearTimeout(timeout)
      reject(new DOMException("Aborted", "AbortError"))
    }
    const timeout = setTimeout(() => {
      signal.removeEventListener("abort", abortHandler)
      resolve()
    }, Math.min(ms, RETRY_MAX_DELAY))
    signal.addEventListener("abort", abortHandler, { once: true })
  })
}

```

This prevents the system from waiting through long back-off periods when the user has already terminated the operation, improving perceived responsiveness.

## Integration in the Session Processor

The retry logic converges in the session processor located at [`packages/opencode/src/session/processor.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/processor.ts). When an LLM stream throws an error, the processor executes a structured recovery sequence:

```typescript
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
  attempt++
  const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
  SessionStatus.set(input.sessionID, {
    type: "retry",
    attempt,
    message: retry,
    next: Date.now() + delay,
  })
  await SessionRetry.sleep(delay, input.abort).catch(() => {})
  continue   // retry the whole processing loop
}

```

This integration demonstrates the complete retry lifecycle: **error classification**, **delay calculation**, **UI state updates** via `SessionStatus`, **abort-aware waiting**, and **loop continuation** for the next attempt. The processor maintains an attempt counter that persists across the `while (true)` loop, ensuring exponential back-off increases with each consecutive failure.

## Practical Usage Examples

### Using the Generic Retry Helper for Network Operations

Developers can leverage the generic retry utility for any flaky async operation beyond LLM calls:

```typescript
import { retry } from "@opencode/util"

async function fetchWithRetry(url: string) {
  return retry(() => fetch(url).then((r) => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    return r.json()
  }), { attempts: 5, delay: 1000 })
}

```

This example configures **five attempts** with a **one-second base delay**, demonstrating how the generic layer supports diverse use cases while maintaining the same exponential back-off semantics.

### Simulating Session Retry Logic in Tests

When testing LLM integrations, developers can simulate the session retry behavior:

```typescript
import { SessionRetry } from "@opencode/opencode/session"

const error = apiError({ "retry-after": "30" }) // mock APIError with header
const attempt = 2

// Determine if retryable
const reason = SessionRetry.retryable(error)

// Compute delay (honors header)
const ms = SessionRetry.delay(attempt, error)

// Execute abort-aware sleep
await SessionRetry.sleep(ms, AbortSignal.timeout(35_000))

```

This test pattern mirrors the production flow exactly, allowing verification that custom `Retry-After` values and exponential back-off calculations behave correctly under controlled conditions.

## Summary

OpenCode's retry logic for failed LLM operations operates through a clean separation of concerns:

- **Generic retry utility** ([`packages/util/src/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/util/src/retry.ts)) provides reusable exponential back-off for any async function, with configurable attempts, delays, and transient error detection.
- **Session-specific retry logic** ([`packages/opencode/src/session/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/retry.ts)) adds LLM domain intelligence, filtering non-retryable errors like context overflows while parsing HTTP `Retry-After` headers for provider-specific delays.
- **Abort-aware execution** ensures that user-initiated cancellations immediately terminate retry loops, preventing wasted resources during back-off periods.
- **Processor integration** ([`packages/opencode/src/session/processor.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/processor.ts)) orchestrates the complete lifecycle, updating UI state via `SessionStatus` while maintaining attempt counters across retry cycles.

## Frequently Asked Questions

### What types of LLM errors trigger a retry in OpenCode?

OpenCode only retries transient, recoverable errors such as rate limits, provider overloads, and network timeouts. Specifically, the `SessionRetry.retryable()` function returns `undefined` for non-retryable errors like `ContextOverflowError`, while returning descriptive messages for retryable `APIError` instances that include flags like `isRetryable: true` or specific provider messages such as "Overloaded".

### How does OpenCode handle HTTP 429 rate limit responses?

When an LLM provider returns a 429 status or includes rate-limit headers, OpenCode's `SessionRetry.delay()` function prioritizes the provider's guidance over calculated back-off. It parses `retry-after-ms` (milliseconds), `retry-after` (seconds or HTTP-date format), and uses those exact values before falling back to exponential back-off starting at 2000ms with a factor of 2, capped at 30 seconds.

### Can users cancel a retry operation mid-stream?

Yes, OpenCode implements abort-aware retry logic through the `SessionRetry.sleep()` function. This utility accepts an `AbortSignal` and immediately rejects with a `DOMException` named "AbortError" if the user cancels the session during a retry delay. This prevents the system from waiting through long back-off periods when the user has already terminated the operation.

### Where is the retry configuration defined in the codebase?

Retry configuration is distributed across two main locations according to the separation of concerns. The generic retry defaults (3 attempts, 500ms base delay, factor of 2, 10s max) reside in [`packages/util/src/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/util/src/retry.ts). The LLM-specific constants (2000ms initial delay, 30s cap without headers, absolute maximum delay) are defined in [`packages/opencode/src/session/retry.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/session/retry.ts), alongside the `SessionRetry` namespace functions.