How Forge Handles API Request Retries: Configuration and Environment Variables

Forge implements a two-layer retry system that classifies provider errors as retryable and applies exponential backoff using parameters controlled by environment variables like FORGE_RETRY_MAX_ATTEMPTS and FORGE_RETRY_INITIAL_BACKOFF_MS.

Managing transient failures when calling LLM APIs is critical for reliability. In the antinomyhq/forgecode codebase, Forge provides a robust API request retry mechanism that automatically handles rate limits and temporary outages through configurable exponential backoff. This article examines the architecture, source code implementation, and environment variables that govern how Forge retries failed requests.

How Forge's Two-Layer Retry System Works

Forge's retry mechanism operates through two distinct layers working in sequence to ensure failed API calls are retried only when appropriate.

Error Classification Layer

Located in crates/forge_repo/src/provider/retry.rs, the into_retry function examines raw provider errors (from OpenAI, Anthropic, Google, Bedrock) and wraps them as DomainError::Retryable when appropriate. This conversion logic inspects HTTP status codes, transport-level failures, and special Anthropic overload signals.

The classification checks:

  • HTTP status codes against retry_config.status_codes (extracted via get_req_status_code, get_event_req_status_code, or get_api_status_code)
  • Transport errors including timeouts, connection resets, and premature stream closure
  • Anthropic "overloaded" SSE events
if let Some(code) = get_req_status_code(&error)
    .or(get_event_req_status_code(&error))
    .or(get_api_status_code(&error))
    && retry_config.status_codes.contains(&code)
{
    return DomainError::Retryable(error).into();
}

All provider modules (openai.rs, anthropic.rs, google.rs, bedrock.rs) invoke into_retry when mapping request errors to domain errors, ensuring a uniform retry policy across providers.

Retry Execution Layer

Found in crates/forge_app/src/retry.rs, the retry_with_config function implements the actual retry logic using the backon crate. It constructs an ExponentialBuilder from RetryConfig parameters and executes the asynchronous operation with exponential backoff. The should_retry predicate ensures only DomainError::Retryable variants trigger retries.

RetryConfig Structure and Defaults

The RetryConfig struct defined in crates/forge_config/src/retry.rs controls all retry behavior with the following fields:

pub struct RetryConfig {
    pub initial_backoff_ms: u64,   // initial delay before first retry
    pub min_delay_ms: u64,        // lower bound for each back-off step
    pub backoff_factor: u64,      // exponential multiplier
    pub max_attempts: usize,      // how many attempts total (including first)
    pub status_codes: Vec<u16>,   // HTTP codes that trigger a retry
    pub max_delay_secs: Option<u64>, // optional ceiling for back-off
    pub suppress_errors: bool,    // if true, retry errors are not logged
}

When Forge initializes, it loads these values from environment variables or uses sensible defaults built into the configuration layer.

Environment Variables That Control Forge Retry Logic

Forge reads retry parameters from environment variables (or a .env file) when constructing the global ForgeConfig. These variables override the default values in RetryConfig:

  • FORGE_RETRY_INITIAL_BACKOFF_MS: Initial back-off delay in milliseconds before the first retry (default: 1000)
  • FORGE_RETRY_BACKOFF_FACTOR: Multiplication factor for exponential backoff calculations (default: 2)
  • FORGE_RETRY_MAX_ATTEMPTS: Maximum number of attempts including the initial call (default: 3)
  • FORGE_RETRY_STATUS_CODES: Comma-separated list of HTTP status codes that trigger retries (default: 429,500,502,503,504)
  • FORGE_SUPPRESS_RETRY_ERRORS: When set to true, suppresses logging of retry-related error messages (default: false)

These variables are documented in the repository README and parsed during the configuration loading phase.

Implementing Exponential Backoff

The retry_with_config function in crates/forge_app/src/retry.rs builds a backon ExponentialBuilder using the RetryConfig fields:

let strategy = ExponentialBuilder::default()
    .with_min_delay(Duration::from_millis(config.min_delay_ms))
    .with_factor(config.backoff_factor as f32)
    .with_max_times(config.max_attempts)
    .with_jitter();

let retryable = operation.retry(&strategy).when(should_retry);

If a notification callback is supplied, it is invoked on each retry attempt; otherwise the function awaits the retryable future silently.

Configuring Retries in Practice

Set environment variables in your .env file or export them in your shell:

export FORGE_RETRY_INITIAL_BACKOFF_MS=500
export FORGE_RETRY_BACKOFF_FACTOR=3
export FORGE_RETRY_MAX_ATTEMPTS=5
export FORGE_RETRY_STATUS_CODES=429,500,502,503,504
export FORGE_SUPPRESS_RETRY_ERRORS=true

You can verify active settings at runtime through the UI layer in crates/forge_main/src/info.rs, which renders the "RETRY CONFIGURATION" section during startup.

Example usage with the retry wrapper:

use forge_app::retry::retry_with_config;
use forge_config::RetryConfig;

let retry_cfg = RetryConfig {
    initial_backoff_ms: 500,
    backoff_factor: 3,
    max_attempts: 5,
    status_codes: vec![429, 500, 502, 503, 504],
    suppress_errors: true,
    ..Default::default()
};

let result = retry_with_config(&retry_cfg, async {
    // Your async API call here
    call_llm_api().await
}, Some(|err, delay| {
    eprintln!("Retrying after {:?} due to: {}", delay, err);
})).await;

Summary

  • Forge uses a two-layer system: error classification in crates/forge_repo/src/provider/retry.rs and exponential backoff execution in crates/forge_app/src/retry.rs.
  • The RetryConfig struct in crates/forge_config/src/retry.rs defines all tunable parameters including delays, factors, and status codes.
  • Environment variables like FORGE_RETRY_MAX_ATTEMPTS and FORGE_RETRY_INITIAL_BACKOFF_MS control runtime behavior without code changes.
  • The backon crate provides the underlying exponential backoff algorithm with jitter support.
  • Provider-specific errors are normalized through the into_retry function, ensuring consistent handling across OpenAI, Anthropic, Google, and Bedrock integrations.

Frequently Asked Questions

What triggers a retry in Forge?

A retry triggers when the into_retry function in crates/forge_repo/src/provider/retry.rs classifies an error as DomainError::Retryable. This occurs when the HTTP status code matches the configured list (429, 500-level errors by default), or when the error represents transport-level failures like timeouts, connection resets, or Anthropic-specific "overloaded" signals.

How do I increase the number of retry attempts?

Set the FORGE_RETRY_MAX_ATTEMPTS environment variable to your desired count. This value includes the initial attempt, so setting it to 5 allows for 1 initial call plus 4 retries. The default value is 3 attempts total.

Can I disable retry error logging?

Yes. Set FORGE_SUPPRESS_RETRY_ERRORS=true in your environment. When enabled, the suppress_errors field in RetryConfig prevents retry-related error messages from appearing in logs, though the retries still occur according to the configured strategy.

Which HTTP status codes are retried by default?

By default, Forge retries the following status codes as specified in FORGE_RETRY_STATUS_CODES: 429 (Too Many Requests), 500 (Internal Server Error), 502 (Bad Gateway), 503 (Service Unavailable), and 504 (Gateway Timeout). You can override this list to include additional codes or restrict it to specific error types.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →