Skip to content

Configuration

Everything django-ox reads lives in the standard TASKS setting, plus three management commands. A full entry with every option spelled out:

TASKS = {
    "default": {
        "BACKEND": "django_ox.backend.OxBackend",
        "QUEUES": ["default", "emails"],
        "OPTIONS": {
            "MAX_ATTEMPTS": 3,
            "LOCK_TIMEOUT": 300,
            "BACKOFF_INITIAL": 5,
            "BACKOFF_MAX": 600,
            "SCHEDULES": {},  # see the Recurring tasks page
        },
    }
}

The smallest working entry

Every option has a default. This is enough to run tasks:

TASKS = {
    "default": {
        "BACKEND": "django_ox.backend.OxBackend",
    }
}

That gives you the default queue, three attempts, and a five second first retry. Add options when you have a reason to.

Backend entry

Key Default Meaning
BACKEND required "django_ox.backend.OxBackend".
QUEUES ["default"] Queue names tasks may be enqueued to. An empty list ([]) allows any queue name. Read by Django's Tasks framework itself.
OPTIONS {} Backend options, below.

OPTIONS

Key Default Meaning
MAX_ATTEMPTS 3 Executions a task gets before it is marked FAILED. An attempt is consumed when a worker claims the task, so a worker dying mid-run counts too and retries stay bounded.
LOCK_TIMEOUT 300 Seconds a RUNNING task's lock may go unrefreshed before the reaper takes the task back. A worker refreshes the lock every LOCK_TIMEOUT / 3 seconds while it is executing, so this is a limit on how long a worker may be unresponsive, not on how long a task may run.
BACKOFF_INITIAL 5 Delay in seconds before the first retry.
BACKOFF_MAX 600 Ceiling on the retry delay, in seconds.
SCHEDULES {} Recurring task definitions. Documented on the Recurring tasks page.

The retry delay after attempt n fails is BACKOFF_INITIAL * 2 ** (n - 1), capped at BACKOFF_MAX. With the defaults: 5 s, 10 s, 20 s, 40 s, and so on up to 600 s. There is no jitter.

ox_worker

python manage.py ox_worker [options]
Flag Default Meaning
--backend default Backend alias from the TASKS setting.
--queues all configured queues Comma-separated queue names this worker processes.
--concurrency 1 Tasks executed concurrently, as a thread pool inside the process.
--interval 1.0 Polling interval in seconds when idle. When tasks are in flight the worker wakes as soon as one finishes, so this does not bound throughput.
--lock-timeout backend LOCK_TIMEOUT, or 300 Seconds a RUNNING task's lock may go unrefreshed before the task is reclaimed.

The command also honors Django's standard -v/--verbosity: at the default verbosity it logs worker lifecycle and warnings to stderr, and -v 2 enables debug logging. -v 0 attaches no log handler.

Two intervals are derived rather than flagged:

  • The reaper runs every min(30, max(lock_timeout / 2, 1)) seconds.
  • Lease renewal runs every max(lock_timeout / 3, 0.1) seconds, on its own thread, and keeps running until the last in-flight task has drained.
  • Schedule dispatch (when SCHEDULES is configured) runs every max(1, min(interval, 30)) seconds, about once a second at the default polling interval.

Routing a queue to its own worker

Declare every queue on the backend, then give each worker a subset. Slow work stops blocking fast work without a second backend or a second database.

TASKS = {
    "default": {
        "BACKEND": "django_ox.backend.OxBackend",
        "QUEUES": ["default", "emails", "exports"],
    }
}
python manage.py ox_worker --queues default,emails --concurrency 4
python manage.py ox_worker --queues exports --concurrency 1 --lock-timeout 3600

The exports worker runs one task at a time and tolerates hour-long jobs. The other worker keeps short work moving at four at a time. A queue with no worker assigned to it accumulates tasks and never runs them, so make sure every queue in QUEUES is covered by some worker.

Tasks that run longer than the lock timeout

A long task is not by itself a problem. A worker refreshes the lock on the tasks it is running every LOCK_TIMEOUT / 3 seconds, so an hour-long task on a healthy worker keeps its lease for the hour.

What LOCK_TIMEOUT bounds is how long a worker may stop refreshing before its work is handed to somebody else. Set it above the longest pause you are willing to tolerate from a worker: a long garbage-collection pause, a throttled container, a slow database, a host that swapped. If a queue runs on hardware that stalls, give it its own worker with its own timeout rather than raising the global value and delaying recovery for everything else:

python manage.py ox_worker --queues exports --lock-timeout 7200

If you embed the worker programmatically, django_ox.worker.Worker accepts reap_interval, renew_interval, schedule_interval, backoff_initial and backoff_max keyword overrides in addition to the flag equivalents.

ox_prune

Finished task rows stay in the table until pruned; the queue table doubles as the result store, and django-ox does not guess at your retention needs. Run ox_prune on your own schedule (cron or a systemd timer; there is an example unit on the Production page):

python manage.py ox_prune --older-than 7d
Flag Default Meaning
--older-than 7d Minimum time since the task finished. Accepts 7d, 24h, 90m, 45s, or a plain number of seconds.
--include-failed off Also delete FAILED and LOST rows. By default they are kept, because they hold the per-attempt tracebacks.
--batch-size 1000 Rows per DELETE statement, so pruning a large table never takes a long lock or builds a giant IN clause. Must be at least 1.
--dry-run off Report how many rows would be deleted without deleting any.

Only SUCCESSFUL rows (and, with --include-failed, FAILED rows) whose finished_at is past the cutoff are deleted. READY and RUNNING rows are never touched, whatever their age. Rows from the recurring-schedule tick log are pruned with the same cutoff, always keeping each schedule's most recent tick; that row anchors missed-tick recovery and deleting it would make the schedule re-anchor. The latest tick row of a schedule that has been removed from settings is kept by the same rule; such rows are harmless and can be deleted by hand if unwanted. See Recurring tasks.

ox_health

A health check for cron alerting and container probes: exits 0 when every enabled check passes, non-zero with a one-line reason otherwise. With no flags it verifies only that the database answers.

python manage.py ox_health --max-backlog 1000 --max-age 600
Flag Default Meaning
--queue all queues Restrict the checks to one queue.
--max-backlog off Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future run_after do not count.
--max-age off Fail when the oldest waiting task has waited longer than this many seconds since becoming eligible.
--worker-timeout off Fail when no worker has claimed a task within this many seconds, or no claim was ever recorded.

Check semantics, probe examples, and guidance on which check fits which alert are on the Monitoring page.

System checks

manage.py check validates the setup:

  • django_ox.E001: django_ox is missing from INSTALLED_APPS.
  • django_ox.E002: a SCHEDULES entry is invalid (task path does not import, cron expression does not parse or can never fire, arguments not JSON-serializable, bad queue name or priority).
  • django_ox.E003: the same schedule name is defined on more than one backend; schedule names must be unique across backends.

The worker performs the same schedule validation at startup, so a bad deploy fails loudly rather than skipping dispatches.