Skip to content

Tasks

Everything you do day-to-day: define a task, enqueue it (with retries and other options), and read the result. For what happens under the hood, see How it works.

Define a task

Use Django's @task decorator — sync (def) or async (async def). It can live in any importable module.

from django.tasks import task


@task
def send_report(user_id: int) -> None:
    ...

Enqueue it

result = send_report.enqueue(42)   # returns a TaskResult; a worker runs it

Enqueuing rides the surrounding database transaction — a task spawned inside atomic() is dropped if the block rolls back.

Retries & spawn options

Absurd's spawn options (retries, retry backoff, idempotency, …) attach two ways.

Per-task defaults — the @absurd_default_params decorator. Apply it below @task:

from django.tasks import task
from django_absurd.params import absurd_default_params


@task
@absurd_default_params(max_attempts=3)   # this task retries up to 3 times
def send_report(user_id: int) -> None:
    ...

Per-enqueue — absurd_spawn_params. Overrides the decorator default for one call:

from django_absurd.params import AbsurdSpawnParams

send_report.enqueue(
    42,
    absurd_spawn_params=AbsurdSpawnParams(
        max_attempts=5,
        retry_strategy={
            "kind": "exponential",   # "fixed" | "exponential" | "none"
            "base_seconds": 2,
            "factor": 2,
            "max_seconds": 300,
        },
        idempotency_key=f"report:{42}",   # enqueue at most once per key
    ),
)

Precedence for max_attempts: per-call → decorator default → OPTIONS["DEFAULT_MAX_ATTEMPTS"] (5).

The fields (types come from absurd_sdk):

Field Where What it does
max_attempts default + per-call Retry ceiling for the task.
retry_strategy default + per-call Backoff: kind (fixed/exponential/none), base_seconds, factor, max_seconds.
cancellation default + per-call max_duration, max_delay (seconds).
headers per-call only Arbitrary JSON metadata carried with the task.
idempotency_key per-call only Dedupe — a repeat enqueue with the same key is a no-op.

Absurd: retries & durable execution.

Read the result

enqueue returns a TaskResult; fetch one later by id:

result = send_report.enqueue(42)

result = send_report.get_result(result.id)     # by id (sync)
result = await send_report.aget_result(result.id)  # async

result.status         # READY | RUNNING | SUCCESSFUL | FAILED
result.return_value   # available once SUCCESSFUL
result.errors         # populated when FAILED

A task may run more than once (at-least-once delivery), so keep handlers idempotent — use idempotency_key (above) where it helps. See retries & runs.