Claude Code for backoff: Retry with Exponential Backoff in Python — Claude Skills 360 Blog
Blog / AI / Claude Code for backoff: Retry with Exponential Backoff in Python
AI

Claude Code for backoff: Retry with Exponential Backoff in Python

Published: March 10, 2028
Read time: 5 min read
By: Claude Skills 360

backoff decorates Python functions with exponential backoff retry logic. pip install backoff. Exception: import backoff; @backoff.on_exception(backoff.expo, requests.exceptions.RequestException). Multiple: @backoff.on_exception(backoff.expo, (ValueError, IOError)). Max tries: @backoff.on_exception(backoff.expo, Exception, max_tries=5). Max time: @backoff.on_exception(backoff.expo, Exception, max_time=60). Predicate: @backoff.on_predicate(backoff.expo, lambda r: r.status_code == 429, max_tries=8). Expo: backoff.expo — base=2, factor=1: delays 1,2,4,8… seconds. backoff.expo(base=2, factor=0.5). Fibo: backoff.fibo — 1,1,2,3,5,8… Constant: backoff.constant(interval=2) — retry every 2 seconds. Jitter: jitter=backoff.full_jitter (default) — uniform random up to delay. jitter=backoff.random_jitter — Gaussian. jitter=None — no jitter. Callbacks: on_backoff=lambda d: print(d) on_success=... on_giveup=.... Details dict: d["tries"] d["elapsed"] d["wait"] d["exception"]. Raise: on giveup, last exception is re-raised. Async: works with async def — just add the decorator. @backoff.on_exception(backoff.expo, aiohttp.ClientError, max_tries=5). Rate limit: @backoff.on_exception(backoff.expo, RateLimitError, max_time=120, jitter=backoff.full_jitter). Claude Code generates backoff HTTP clients, rate-limit handlers, and resilient API wrappers.

CLAUDE.md for backoff

## backoff Stack
- Version: backoff >= 2.2 | pip install backoff
- Exception: @backoff.on_exception(backoff.expo, RequestException, max_tries=5)
- Predicate: @backoff.on_predicate(backoff.expo, lambda r: r is None, max_time=30)
- Wait: backoff.expo (geometric) | backoff.fibo (Fibonacci) | backoff.constant
- Limits: max_tries=N | max_time=seconds (wall-clock limit)
- Jitter: jitter=backoff.full_jitter (default) | jitter=None for exact delays
- Callbacks: on_backoff=fn | on_giveup=fn | on_success=fn
- Async: works transparently with async def functions

backoff Exponential Retry Pipeline

# app/retry_utils.py — backoff decorators, HTTP retry wrappers, and rate-limit handlers
from __future__ import annotations

import logging
import time
from typing import Any, Callable

import backoff

logger = logging.getLogger(__name__)


# ─────────────────────────────────────────────────────────────────────────────
# 1. Callbacks
# ─────────────────────────────────────────────────────────────────────────────

def _log_backoff(details: dict) -> None:
    """Standard backoff callback: log the retry attempt."""
    exc = details.get("exception")
    logger.warning(
        "Retrying %s (attempt %d, %.1fs elapsed, waiting %.2fs): %s",
        details["target"].__name__,
        details["tries"],
        details["elapsed"],
        details["wait"],
        exc,
    )


def _log_giveup(details: dict) -> None:
    """Standard giveup callback: log the final failure."""
    exc = details.get("exception")
    logger.error(
        "Gave up %s after %d tries (%.1fs): %s",
        details["target"].__name__,
        details["tries"],
        details["elapsed"],
        exc,
    )


def _log_success(details: dict) -> None:
    """Success callback: log if the function succeeded after retries."""
    if details["tries"] > 1:
        logger.info(
            "%s succeeded on try %d (%.1fs elapsed)",
            details["target"].__name__,
            details["tries"],
            details["elapsed"],
        )


# ─────────────────────────────────────────────────────────────────────────────
# 2. Reusable decorator factories
# ─────────────────────────────────────────────────────────────────────────────

def retry_on_exception(
    *exceptions,
    max_tries: int = 5,
    max_time: float | None = None,
    base: float = 2,
    factor: float = 1,
    jitter=backoff.full_jitter,
    log: bool = True,
):
    """
    Return a decorator that retries on any of the given exceptions.
    Uses exponential backoff: delay = factor * base^n (+ jitter).
    max_tries and max_time are both respected — whichever triggers first.
    """
    kwargs = dict(
        wait_gen=backoff.expo(base=base, factor=factor),
        exception=exceptions,
        max_tries=max_tries,
        jitter=jitter,
    )
    if max_time is not None:
        kwargs["max_time"] = max_time
    if log:
        kwargs.update(on_backoff=_log_backoff, on_giveup=_log_giveup)
    return backoff.on_exception(**kwargs)


def retry_on_predicate(
    predicate: Callable,
    max_tries: int = 5,
    max_time: float | None = None,
    base: float = 2,
    factor: float = 1,
    jitter=backoff.full_jitter,
    log: bool = True,
):
    """
    Return a decorator that retries when `predicate(return_value)` is truthy.
    Use for polling patterns: retry while result is None or status is "pending".
    """
    kwargs = dict(
        wait_gen=backoff.expo(base=base, factor=factor),
        predicate=predicate,
        max_tries=max_tries,
        jitter=jitter,
    )
    if max_time is not None:
        kwargs["max_time"] = max_time
    if log:
        kwargs["on_backoff"] = _log_backoff
    return backoff.on_predicate(**kwargs)


# ─────────────────────────────────────────────────────────────────────────────
# 3. Pre-built decorator presets
# ─────────────────────────────────────────────────────────────────────────────

# HTTP / network errors
def http_retry(max_tries: int = 5, max_time: float = 60):
    """Decorator: retry on common HTTP/network exceptions."""
    try:
        import requests
        network_errors = (requests.exceptions.ConnectionError,
                          requests.exceptions.Timeout,
                          requests.exceptions.HTTPError)
    except ImportError:
        import urllib.error
        network_errors = (urllib.error.URLError, OSError)

    return retry_on_exception(*network_errors, max_tries=max_tries, max_time=max_time)


def rate_limit_retry(max_time: float = 120, max_tries: int = 10):
    """
    Decorator: retry on rate-limit responses.
    Works with custom RateLimitError — raise it in your request wrapper
    when you see HTTP 429 or 503.
    """
    return retry_on_exception(
        RateLimitError,
        max_tries=max_tries,
        max_time=max_time,
        base=2,
        factor=2,            # start with 2s delay: 2, 4, 8, 16...
        jitter=backoff.full_jitter,
    )


def transient_retry(max_tries: int = 3, interval: float = 1.0):
    """Decorator: retry with constant interval for transient errors."""
    return backoff.on_exception(
        backoff.constant(interval=interval),
        Exception,
        max_tries=max_tries,
        jitter=None,
        on_backoff=_log_backoff,
    )


# ─────────────────────────────────────────────────────────────────────────────
# 4. Custom exception classes
# ─────────────────────────────────────────────────────────────────────────────

class RateLimitError(Exception):
    """Raised when an API returns HTTP 429 or a custom rate-limit signal."""
    def __init__(self, retry_after: float | None = None):
        self.retry_after = retry_after
        super().__init__(f"Rate limited (retry_after={retry_after})")


class TransientError(Exception):
    """Raised for errors that are expected to resolve on retry."""


class ServerError(Exception):
    """Raised for 5xx HTTP responses."""


# ─────────────────────────────────────────────────────────────────────────────
# 5. HTTP request wrapper with retry
# ─────────────────────────────────────────────────────────────────────────────

def make_http_get(
    max_tries: int = 5,
    max_time: float = 60.0,
    retry_statuses: tuple[int, ...] = (429, 500, 502, 503, 504),
):
    """
    Return a get() function that retries on transient HTTP errors.
    Raises RateLimitError on 429 so rate_limit_retry() can be used.
    """
    import requests

    @backoff.on_exception(
        backoff.expo,
        (requests.exceptions.ConnectionError,
         requests.exceptions.Timeout,
         RateLimitError,
         ServerError),
        max_tries=max_tries,
        max_time=max_time,
        on_backoff=_log_backoff,
        on_giveup=_log_giveup,
        on_success=_log_success,
    )
    def get(url: str, **kwargs) -> "requests.Response":
        resp = requests.get(url, timeout=kwargs.pop("timeout", 10), **kwargs)
        if resp.status_code == 429:
            retry_after = float(resp.headers.get("Retry-After", 0))
            raise RateLimitError(retry_after=retry_after)
        if resp.status_code in (500, 502, 503, 504):
            raise ServerError(f"HTTP {resp.status_code}: {url}")
        resp.raise_for_status()
        return resp

    return get


# ─────────────────────────────────────────────────────────────────────────────
# 6. Async variants
# ─────────────────────────────────────────────────────────────────────────────

async def async_retry_example():
    """backoff works transparently with async functions."""
    import asyncio

    @backoff.on_exception(
        backoff.expo,
        (IOError, RuntimeError),
        max_tries=4,
        on_backoff=_log_backoff,
    )
    async def flaky_fetch(url: str) -> str:
        # Simulated async operation
        await asyncio.sleep(0.01)
        return f"data from {url}"

    return await flaky_fetch("https://example.com")


# ─────────────────────────────────────────────────────────────────────────────
# 7. Polling with on_predicate
# ─────────────────────────────────────────────────────────────────────────────

def poll_until_ready(
    check_fn: Callable[[], Any],
    predicate: Callable[[Any], bool] | None = None,
    max_tries: int = 20,
    max_time: float = 120.0,
) -> Any:
    """
    Poll `check_fn()` until `predicate(result)` is falsy (i.e., ready).
    Default predicate: retry while result is None.
    """
    if predicate is None:
        predicate = lambda r: r is None

    @backoff.on_predicate(
        backoff.expo,
        predicate=predicate,
        max_tries=max_tries,
        max_time=max_time,
        jitter=backoff.full_jitter,
        on_backoff=_log_backoff,
    )
    def _poll():
        return check_fn()

    return _poll()


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import random

    call_count = [0]

    # ── Demo 1: on_exception with attempt counter ──────────────────────────
    @backoff.on_exception(
        backoff.expo,
        (ValueError,),
        max_tries=4,
        jitter=None,
        on_backoff=lambda d: print(f"  Retry #{d['tries']} (wait {d['wait']:.2f}s)"),
    )
    def flaky_function():
        call_count[0] += 1
        if call_count[0] < 3:
            raise ValueError(f"Transient error #{call_count[0]}")
        return f"success on call #{call_count[0]}"

    print("=== on_exception (succeeds on 3rd try) ===")
    result = flaky_function()
    print(f"  Result: {result}")

    # ── Demo 2: on_predicate ───────────────────────────────────────────────
    poll_count = [0]

    @backoff.on_predicate(
        backoff.expo,
        lambda r: r != "done",
        max_tries=5,
        jitter=None,
        on_backoff=lambda d: print(f"  Poll #{d['tries']}: not ready yet"),
    )
    def check_status():
        poll_count[0] += 1
        return "done" if poll_count[0] >= 3 else "pending"

    print("\n=== on_predicate (polling) ===")
    status = check_status()
    print(f"  Final status: {status}")

    # ── Demo 3: giveup after max_tries ─────────────────────────────────────
    print("\n=== on_exception (giveup) ===")
    giveup_called = [False]

    @backoff.on_exception(
        backoff.constant(interval=0.01),
        RuntimeError,
        max_tries=3,
        jitter=None,
        on_giveup=lambda d: giveup_called.__setitem__(0, True),
    )
    def always_fails():
        raise RuntimeError("always bad")

    try:
        always_fails()
    except RuntimeError:
        print(f"  RuntimeError raised after 3 tries | on_giveup called: {giveup_called[0]}")

    # ── Demo 4: Fibonacci backoff ──────────────────────────────────────────
    fib_waits = []

    @backoff.on_exception(
        backoff.fibo,
        ValueError,
        max_tries=6,
        jitter=None,
        on_backoff=lambda d: fib_waits.append(round(d["wait"], 2)),
    )
    def fibonacci_target():
        raise ValueError("trigger fib")

    try:
        fibonacci_target()
    except ValueError:
        pass
    print(f"\n=== Fibonacci wait sequence ===\n  {fib_waits}")

For the tenacity alternative — tenacity and backoff both provide retry logic via decorators; backoff uses a functional/generator-based API (backoff.expo, backoff.fibo) and is more concise for simple cases; tenacity uses a richer DSL with retry_if_exception_type, wait_exponential, stop_after_attempt, before_sleep all composable via | operators — use tenacity when you need combine multiple stop/wait/retry strategies, and backoff when the decorator-first API is more readable. For the retry (retry package) alternative — the retry package (pip install retry) provides @retry and @retry(tries=3, delay=2, backoff=2) but is less maintained and lacks async support, predicate-based retry, and separate jitter configurations; backoff is more actively maintained and covers async, full_jitter, giveup callbacks, and Fibonacci/constant wait generators. The Claude Skills 360 bundle includes backoff skill sets covering @backoff.on_exception with exception tuple, @backoff.on_predicate for polling, backoff.expo/fibo/constant wait generators, max_tries/max_time limits, full_jitter/random_jitter, on_backoff/on_giveup/on_success callbacks, retry_on_exception() and retry_on_predicate() decorator factories, http_retry() and rate_limit_retry() presets, RateLimitError/TransientError/ServerError custom exceptions, make_http_get() HTTP retry wrapper, poll_until_ready() polling helper, and async function support. Start with the free tier to try exponential backoff retry code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free