Tasks¶
Enqueue background work and read the result. A worker runs it.
Enqueue¶
from django.tasks import task
@task
def send_report(user_id: int) -> None: ...
result = send_report.enqueue(42) # returns a TaskResult; a worker runs it
A @task lives in any importable
module. async def works the same — await send_report.aenqueue(42).
- Enqueuing rides the surrounding transaction, so an
atomic()rollback drops the task. - Delivery is at-least-once — keep handlers idempotent. See runs & retries.
Read the result¶
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
Ids are "<queue>:<uuid>" — the same value context.task_result.id reports inside a
takes_context task, so either can go straight back to get_result.
Run it later¶
Django's
run_after
defers one enqueue, taking a timezone-aware datetime. For a repeating schedule, use
Cron Jobs.
- A wrapper row named
<task path>:run_afterwaits, then enqueues yours. Both appear in the admin. - The id you got back keeps working:
READYwhile the wrapper waits, then your task's own status and result. A wrapper that can't launch staysREADYwith no visible error until it exhausts its attempts.
Retries & spawn options¶
Absurd's spawn options — retries, backoff, cancellation, headers, idempotency — attach
through one factory, absurd_params, at two call sites.
Per-task defaults¶
from django.tasks import task
from django_absurd import absurd_params
@task
@absurd_params(max_attempts=3) # apply BELOW @task
def send_report(user_id: int) -> None: ...
Per-invocation¶
from django_absurd import absurd_params
absurd_params(
max_attempts=5,
retry_strategy={
"kind": "exponential", # "fixed" | "exponential" | "none"
"base_seconds": 2,
"factor": 2,
"max_seconds": 300,
},
).bind(send_report).enqueue(42)
bind overrides the decorator default for one call. Precedence for max_attempts:
per-invocation → decorator →
OPTIONS["DEFAULT_MAX_ATTEMPTS"] (5).
| Field | Where | Default | What it does |
|---|---|---|---|
max_attempts |
decorator + per-call | DEFAULT_MAX_ATTEMPTS (5) |
Retry ceiling; None means retry forever. |
retry_strategy |
decorator + per-call | kind: "none" — retry immediately, no backoff |
Backoff: kind (fixed/exponential/none), base_seconds, factor, max_seconds. |
cancellation |
decorator + per-call | unset — no time limit | max_duration, max_delay (seconds). |
headers |
per-call only | unset | Arbitrary JSON metadata carried with the task. |
idempotency_key |
per-call only | unset — no deduping | Dedupe within a queue — see below. |
- Backoff defaults, once you pick a
kind:fixedwaitsbase_seconds(60);exponentialwaitsbase_seconds(30) ×factor(2) ^ (attempt − 1), uncapped unless you setmax_seconds. headersandidempotency_keyon the decorator form are an error, statically and at runtime.bindreturns an ordinaryTask, soaenqueue,call,get_result, andusingall still work.- Django's own options stay on
.using(), never onabsurd_params. They compose in either order. max_attempts=Nonemeans retry forever — and only an explicitNonedoes, since omitting it fills in the default. Such a task is never terminal, so Django's task logger never records a final line.- On a non-Absurd backend the params are inert, with one
WARNINGper task.
→ Absurd: retries & durable execution.
Idempotency keys¶
Whichever enqueue reaches a key first owns it; later ones are swallowed and handed the first task's id. The comparison is the key alone — no task name, no arguments — so namespace it yourself or unrelated work collides:
absurd_params(idempotency_key="nightly").bind(send_report).enqueue(42)
absurd_params(idempotency_key="nightly").bind(purge_cache).enqueue()
# -> same id, and purge_cache never runs
- Scoped to one queue. The same key on
defaultand onreportsreserves independently, and both run. - Held as long as the task row exists — freed only once the task is terminal and
cleanup deletes it,
cleanup_ttl(default 30 days) later. Not "once per hour"; "once until the row is swept". - The beat scheduler namespaces its own: a
cron:-prefixed hash of the schedule name, expression, and slot.