Skip to content

How to Configure Retry with Backoff for Tasks

This guide shows how to configure Celery retry behaviour and exponential backoff for pico-celery worker tasks.

Basic Retry Configuration

Pass Celery task options directly through the @task decorator:

from pico_ioc import component
from pico_celery import task

@component(scope="prototype")
class PaymentTasks:
    def __init__(self, payment_service: PaymentService):
        self.service = payment_service

    @task(
        name="tasks.charge_card",
        max_retries=5,
        default_retry_delay=60,  # seconds
    )
    async def charge_card(self, order_id: int, amount: float):
        try:
            await self.service.charge(order_id, amount)
        except PaymentGatewayError as exc:
            raise self.charge_card.retry(exc=exc)

The max_retries and default_retry_delay options are forwarded to celery_app.task() during registration by PicoTaskRegistrar.

Exponential Backoff

Celery 5 supports automatic exponential backoff via the retry_backoff option:

@component(scope="prototype")
class EmailTasks:
    def __init__(self, mailer: MailerService):
        self.mailer = mailer

    @task(
        name="tasks.send_email",
        bind=True,
        autoretry_for=(ConnectionError, TimeoutError),
        max_retries=5,
        retry_backoff=True,        # enables exponential backoff
        retry_backoff_max=600,     # cap at 10 minutes
        retry_jitter=True,         # add randomness to avoid thundering herd
    )
    async def send_email(self, to: str, subject: str, body: str):
        await self.mailer.send(to, subject, body)

With retry_backoff=True, Celery uses increasing delays: 1s, 2s, 4s, 8s, ... up to retry_backoff_max.

Backoff Options Reference

Option Type Description
max_retries int Maximum number of retry attempts
default_retry_delay int Fixed delay in seconds between retries
retry_backoff bool / int True for default base (1s), or an integer base value
retry_backoff_max int Maximum backoff delay in seconds (default: 600)
retry_jitter bool Add random jitter to backoff delay
autoretry_for tuple Exception types that trigger automatic retry

Manual Retry with Custom Countdown

For full control over retry timing, use self.retry() inside the task:

@component(scope="prototype")
class ImportTasks:
    def __init__(self, importer: DataImporter):
        self.importer = importer

    @task(name="tasks.import_data", bind=True, max_retries=3)
    async def import_data(self, self_task, file_path: str):
        try:
            await self.importer.run(file_path)
        except RateLimitError as exc:
            # Retry after progressively longer delays
            countdown = 2 ** self_task.request.retries * 30
            raise self_task.retry(exc=exc, countdown=countdown)

Note

When using bind=True, Celery passes the task instance as the first argument after self. The pico-celery wrapper preserves this behaviour.

Configuring Retry at the Client Side

When sending tasks from a @celery client, you can also set per-call options:

from pico_celery import celery, send_task

@celery
class PaymentClient:
    @send_task(
        name="tasks.charge_card",
        queue="payments",
        countdown=5,  # delay initial execution by 5 seconds
    )
    def charge_card(self, order_id: int, amount: float):
        pass

These options are forwarded to celery_app.send_task() by the CeleryClientInterceptor.