Skip to main content
Lesson 14 of 22 · Responses API: OpenAI Fundamentals

Retry, Exponential Backoff, and Circuit Breaker

Lesson 14 of 22 Updated July 29, 2026 Developer
On this page
  1. Retry, Exponential Backoff, and Circuit Breaker
  2. Exponential Backoff
  3. Using the Tenacity Library
  4. The Circuit Breaker Pattern
  5. Combining the Three Patterns
  6. Key Takeaways

Retry, Exponential Backoff, and Circuit Breaker

Transient errors are normal in production. The OpenAI SDK automatically handles two retries, but for maximum resilience, you should implement your own strategies. The previous lesson taught you to recognize retryable errors; this one teaches you to retry intelligently—without making the situation you’re trying to fix worse.

Exponential Backoff

The principle is simple: each retry waits longer than the previous one. This prevents overloading the API when it’s under pressure. Jitter deserves special attention because its absence causes a classic failure mode: fifty instances of your service receive a 429 at the same second, all wait exactly one second, then all restart together and get rejected again as a group. By adding a random delay, you spread out this restart—that’s the remedy for the thundering herd.

import time
import random
from openai import OpenAI, RateLimitError, InternalServerError, APIConnectionError

client = OpenAI()

def call_with_backoff(prompt: str, max_retries: int = 5) -> str:
    """API call with exponential backoff and jitter."""

    for attempt in range(max_retries):
        try:
            response = client.responses.create(
                model="gpt-5.6-terra",
                input=prompt
            )
            return response.output_text

        except (RateLimitError, InternalServerError, APIConnectionError) as e:
            if attempt == max_retries - 1:
                raise  # Last attempt, propagate the error

            # Exponential backoff: 1s, 2s, 4s, 8s, 16s
            base_delay = 2 ** attempt

            # Random jitter to avoid thundering herd
            jitter = random.uniform(0, base_delay * 0.5)
            delay = base_delay + jitter

            print(f"Attempt {attempt + 1}/{max_retries} failed. "
                  f"Retrying in {delay:.1f}s...")
            time.sleep(delay)

    raise RuntimeError("Should never reach here")

# Usage
result = call_with_backoff("Explain exponential backoff.")
print(result)

Notice the final attempt: we propagate the original error instead of swallowing it. The caller thus receives the real diagnostic—429 or 500—and can decide what to do with it.

Using the Tenacity Library

Writing this loop by hand quickly becomes repetitive once you have five functions to protect. For more advanced retry management, tenacity is the go-to tool: the decorator declares the policy, and the function body contains only the business logic call. You get the same ingredients, but named and configurable—the list of exceptions to retry, exponential wait capped at sixty seconds, stopping after five attempts, and automatic logging before each pause.

# pip install tenacity
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep_log,
)
from openai import RateLimitError, InternalServerError, APIConnectionError
import logging

logger = logging.getLogger(__name__)

@retry(
    retry=retry_if_exception_type(
        (RateLimitError, InternalServerError, APIConnectionError)
    ),
    wait=wait_exponential(multiplier=1, min=1, max=60),
    stop=stop_after_attempt(5),
    before_sleep=before_sleep_log(logger, logging.WARNING),
)
def resilient_call(prompt: str, model: str = "gpt-5.6-terra") -> str:
    """API call with automatic retry via tenacity."""
    response = client.responses.create(
        model=model,
        input=prompt
    )
    return response.output_text

# Usage — retries are transparent
result = resilient_call("Hello!")
print(result)

The Circuit Breaker Pattern

Retry solves incidents lasting a few seconds. It becomes harmful when the outage lasts longer: each user request then consumes five attempts and thirty seconds of waiting before failing, your threads pile up, and it’s your own service that goes down. The circuit breaker prevents your application from continuing to call a failing API.

It works like an electrical circuit breaker, with three states. Closed, everything works normally and calls pass through. Open, the API is considered down and calls are blocked immediately, without even attempting the network—the user receives an error in milliseconds rather than after a timeout. Half-open, once the rest period has elapsed, a test call is sent to check if the API is back: if it succeeds, the circuit closes; if it fails, it reopens for a new cycle.

import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    """Circuit breaker for API calls."""

    def __init__(self, error_threshold: int = 5, reset_timeout: float = 30.0):
        self.threshold = error_threshold
        self.reset_timeout = reset_timeout
        self.consecutive_errors = 0
        self.state = CircuitState.CLOSED
        self.last_error_time = 0.0

    def can_call(self) -> bool:
        """Checks if a call is allowed."""
        if self.state == CircuitState.CLOSED:
            return True

        if self.state == CircuitState.OPEN:
            if time.monotonic() - self.last_error_time > self.reset_timeout:
                self.state = CircuitState.HALF_OPEN
                return True  # One test call
            return False

        # Half-open: only one call allowed
        return True

    def record_success(self):
        """Records a successful call."""
        self.consecutive_errors = 0
        self.state = CircuitState.CLOSED

    def record_error(self):
        """Records a failure."""
        self.consecutive_errors += 1
        self.last_error_time = time.monotonic()

        if self.consecutive_errors >= self.threshold:
            self.state = CircuitState.OPEN
            print(f"Circuit OPEN after {self.threshold} consecutive errors")

The error counter resets to zero as soon as a call succeeds: it’s consecutive failures that open the circuit, not a historical cumulative count. Client-side integration then comes down to three steps—ask for permission, signal success, signal failure.

# Integration with the OpenAI client
circuit = CircuitBreaker(error_threshold=3, reset_timeout=30.0)

def call_with_circuit_breaker(prompt: str) -> str:
    """API call protected by a circuit breaker."""
    if not circuit.can_call():
        raise RuntimeError(
            f"Circuit open — API unavailable. "
            f"Retry in {circuit.reset_timeout}s"
        )

    try:
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=prompt
        )
        circuit.record_success()
        return response.output_text

    except (RateLimitError, InternalServerError, APIConnectionError) as e:
        circuit.record_error()
        raise

# Usage
try:
    result = call_with_circuit_breaker("Hello!")
    print(result)
except RuntimeError as e:
    print(f"Error: {e}")

Combining the Three Patterns

In production, combine backoff, circuit breaker, and timeout. Notice the constructor: the client is created with max_retries=0, which disables the SDK’s internal retries. Without this, your three attempts would multiply by the SDK’s two, and you’d wait much longer than expected. The thirty-second timeout bounds each individual call, the loop handles retries, and the circuit breaker monitors the overall health of the service.

class APIClient:
    """Resilient API client combining all patterns."""

    def __init__(self):
        self.client = OpenAI(timeout=30.0, max_retries=0)
        self.circuit = CircuitBreaker(error_threshold=5, reset_timeout=60.0)

    def call(self, prompt: str, max_retries: int = 3) -> str:
        if not self.circuit.can_call():
            raise RuntimeError("Service temporarily unavailable")

        for attempt in range(max_retries):
            try:
                response = self.client.responses.create(
                    model="gpt-5.6-terra",
                    input=prompt
                )
                self.circuit.record_success()
                return response.output_text

            except (RateLimitError, InternalServerError) as e:
                self.circuit.record_error()

                if attempt < max_retries - 1:
                    delay = (2 ** attempt) + random.uniform(0, 1)
                    time.sleep(delay)
                else:
                    raise

        raise RuntimeError("Failed after all attempts")

# Usage
api = APIClient()
print(api.call("Explain resilience in software engineering."))

Before moving on, test this client by simulating an outage: cut the network, launch a dozen calls, and verify that the last ones fail instantly instead of waiting. If that’s not the case, your threshold is too high for the volume being processed.

Key Takeaways

  • The SDK handles 2 retries by default — adjust with max_retries
  • Exponential backoff with jitter avoids the thundering herd
  • The circuit breaker protects your application from prolonged outages
  • Use tenacity for declarative retry management in production
  • Combine all three patterns for maximum resilience
  • Never retry 400/401/403 errors (client errors)