Skip to content

Getting Started

This guide will walk you through the basics of using backoff for retrying functions.

Installation

Install backoff using pip:

pip install python-backoff

Supported Python versions

backoff requires Python 3.10 or newer. Support for Python 3.7, 3.8 and 3.9 was dropped in v2.3.0, v2.4.0 and v3.0.0 respectively.

Basic Concepts

Backoff provides two main decorators:

  1. @backoff.on_exception - Retry when a specific exception is raised
  2. @backoff.on_predicate - Retry when a condition is true about the return value

Your First Retry

Let's start with a simple example - retrying a network request:

import requests

import backoff


@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
)
def get_url(url):
    return requests.get(url)

This decorator will:

  • Retry whenever RequestException (or any subclass) is raised
  • Use exponential backoff (wait times: 1s, 2s, 4s, 8s, 16s, ...)
  • Keep retrying indefinitely until success

Adding Limits

In production, you'll want to limit retries:

@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    max_time=60,
    max_tries=5,
)
def get_url(url):
    return requests.get(url)

This will give up after either:

  • 60 seconds have elapsed, OR
  • 5 retry attempts have been made

Handling Multiple Exceptions

You can retry on multiple exception types:

@backoff.on_exception(
    backoff.expo,
    (
        requests.exceptions.Timeout,
        requests.exceptions.ConnectionError,
    ),
    max_time=30,
)
def get_url(url):
    return requests.get(url)

Conditional Give-Up

Sometimes you need custom logic to decide when to stop retrying:

def fatal_code(e):
    """Don't retry on 4xx errors"""
    return 400 <= e.response.status_code < 500


@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    max_time=300,
    giveup=fatal_code,
)
def get_url(url):
    return requests.get(url)

Using on_predicate

For polling or checking return values:

@backoff.on_predicate(
    backoff.constant,
    lambda result: result is None,
    interval=5,
    max_time=300,
)
def check_job_status(job_id):
    response = requests.get(f"/jobs/{job_id}")
    if response.json()["status"] == "complete":
        return response.json()
    return None  # Will trigger retry

Wait Strategies

Backoff provides several wait strategies:

Exponential (expo)

@backoff.on_exception(backoff.expo, Exception)
def my_function(): ...

Wait times: 1s, 2s, 4s, 8s, 16s, ...

Fibonacci (fibo)

@backoff.on_exception(backoff.fibo, Exception)
def my_function(): ...

Wait times: 1s, 1s, 2s, 3s, 5s, 8s, 13s, ...

Constant

@backoff.on_exception(
    backoff.constant,
    Exception,
    interval=5,
)
def my_function(): ...

Wait times: 5s, 5s, 5s, 5s, ...

Event Handlers

Track what's happening during retries:

def log_backoff(details):
    print(f"Backing off {details['wait']:.1f} seconds after {details['tries']} tries")


def log_success(details):
    print(f"Success after {details['tries']} tries")


@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    on_backoff=log_backoff,
    on_success=log_success,
    max_tries=5,
)
def get_url(url):
    return requests.get(url)

Async Support

Backoff works seamlessly with async functions:

import aiohttp


@backoff.on_exception(
    backoff.expo,
    aiohttp.ClientError,
    max_time=60,
)
async def get_url(url):
    async with aiohttp.ClientSession() as session, session.get(url) as response:
        return await response.text()

Next Steps