Django Q2 Background Jobs
Use this before touching task enqueueing, schedules, worker deployment,
Q_CLUSTER, or code imported by Django Q2 workers.
Project Defaults
- The dependency is
django-q2; the Python import path is django_q.
django_q is in INSTALLED_APPS; its migrations provide task result,
schedule, and broker models.
Q_CLUSTER lives in the generated Django settings module and uses Redis
through REDIS_URL by default.
- The worker process is
uv run --no-sync python manage.py qcluster; the local
workers Docker Compose service and deployment APP_PROCESS_TYPE=worker
path use that command.
- Redis also backs the default Django cache. If you remove Redis as the broker,
review cache, health check, Docker, deployment, and documentation references
separately.
Mental Model
- Web code calls
async_task(...) or creates Schedule rows.
- The broker stores queued task packages until a
qcluster process reserves
work.
- Worker processes execute importable Python functions and save results or
failures.
- The scheduler runs inside the cluster. Schedules are database rows; they do
nothing unless
qcluster is running.
Adding Tasks
- Put the task function in the app that owns the behavior, usually
apps/core/tasks.py for shared product work.
- Keep the function importable at module import time. Do not rely on request
objects, local closures, or process-local state.
- Pass durable identifiers such as primary keys, not model instances, open
files, connections, or large payloads.
- Make the task idempotent. Redis does not give exactly-once execution
guarantees, and receipt-based brokers can re-run work.
- If the task depends on a just-saved database row, enqueue it from
transaction.on_commit(...).
# apps/core/tasks.py
def send_welcome_email(user_id: int) -> None:
from django.contrib.auth import get_user_model
user = get_user_model().objects.get(pk=user_id)
...
from django.db import transaction
from django_q.tasks import async_task
transaction.on_commit(
lambda: async_task("apps.core.tasks.send_welcome_email", user.pk)
)
Use q_options when Django Q2 options would collide with task kwargs:
async_task(
"apps.core.tasks.rebuild_report",
report_id,
q_options={"timeout": 300, "group": "reports"},
)
Scheduling Work
Prefer named, idempotent schedules created by a migration, admin action, or
setup command. Avoid creating schedules unconditionally at import time or app
startup.
from django_q.models import Schedule
Schedule.objects.get_or_create(
name="clear-expired-sessions",
defaults={
"func": "django.core.management.call_command",
"args": "'clearsessions'",
"schedule_type": Schedule.HOURLY,
},
)
Use Schedule.objects.get_or_create(name=..., defaults={...}) when seeding
schedules so repeated setup does not duplicate jobs. Cron schedules require the
optional croniter dependency; do not use Schedule.CRON unless the generated
project includes it.
Missed schedules catch up by default. Set Q_CLUSTER["catch_up"] = False when
a job should run once after downtime instead of replaying every missed interval.
Broker Choices
Redis Broker
This template should normally use Redis:
Q_CLUSTER = {
"name": "...",
"timeout": 3600,
"workers": 4,
"redis": REDIS_URL,
}
Redis is fast and matches local Docker, deployment workers, and the default
cache configuration. The default Redis broker does not support delivery
receipts. If a worker host dies catastrophically while executing a task, the
in-flight package can be lost; if task code raises, Django Q2 records a
failure. Use idempotent task design, explicit retries in task code where
needed, and monitoring for failures.
ORM Broker
Use the Django database broker only for low-throughput deployments, local
simplicity, or environments where Redis is unavailable:
Q_CLUSTER = {
"name": "...",
"timeout": 3600,
"retry": 4800,
"workers": 4,
"max_attempts": 2,
"orm": "default",
}
When switching to ORM:
- Remove the
"redis" broker key; configure one broker per cluster unless you
intentionally use custom clusters.
- Run migrations for
django_q. If the broker uses a non-default database
alias, run migrations with --database <alias>.
- Increase
"poll" above the default 0.2 seconds, for example "poll": 2.0,
when you need lower database polling pressure and can tolerate higher queue
pickup latency.
- The ORM broker enables the Queued Tasks admin table.
- Review Redis-dependent cache, health check, Docker, and deployment settings
separately. Schedules are always database rows; the broker setting controls
queued task packages, not the schedule table.
Testing
- Test task business logic by calling the function directly.
- Test enqueueing with synchronous mode:
- per call:
async_task("apps.core.tasks.fn", arg, sync=True)
- per test: override
Q_CLUSTER["sync"] = True
- For worker/broker integration, run
uv run --no-sync python manage.py qcluster in a separate process and wait
for result(task_id, 200) or a similar bounded wait; do not rely on
arbitrary sleeps.
- Use
pytest.mark.django_db(transaction=True) when a real worker process must
observe committed database rows.
Debugging Checklist
- Is a
qcluster process running with the same settings module, SECRET_KEY,
broker URL, and cluster name as the web process?
- Can the worker import the dotted task path?
- Did database migrations run, including
django_q migrations?
- Is Redis reachable from both web and worker containers, or is the ORM broker
polling the expected database?
- Did a scheduled task duplicate because setup created another
Schedule row
with no stable name?
- Did downtime trigger schedule catch-up?
- Is task failure visible in Django admin, logs, or the configured error
reporter?
References
1---2name: django-q23description: "Use when adding, changing, testing, or debugging Django Q2 background tasks, scheduled jobs, qcluster workers, Redis broker configuration, or ORM broker fallback in this generated Django SaaS app."4license: MIT5compatibility: Codex, Claude Code, and other Agent Skills-compatible clients.6metadata:7 version: "0.1.0"8 displayName: Django Q29 category: Django10 tags: django,django-q2,background-jobs,redis11---1213# Django Q2 Background Jobs1415Use this before touching task enqueueing, schedules, worker deployment,16`Q_CLUSTER`, or code imported by Django Q2 workers.1718## Project Defaults1920- The dependency is `django-q2`; the Python import path is `django_q`.21- `django_q` is in `INSTALLED_APPS`; its migrations provide task result,22 schedule, and broker models.23- `Q_CLUSTER` lives in the generated Django settings module and uses Redis24 through `REDIS_URL` by default.25- The worker process is `uv run --no-sync python manage.py qcluster`; the local26 `workers` Docker Compose service and deployment `APP_PROCESS_TYPE=worker`27 path use that command.28- Redis also backs the default Django cache. If you remove Redis as the broker,29 review cache, health check, Docker, deployment, and documentation references30 separately.3132## Mental Model3334- Web code calls `async_task(...)` or creates `Schedule` rows.35- The broker stores queued task packages until a `qcluster` process reserves36 work.37- Worker processes execute importable Python functions and save results or38 failures.39- The scheduler runs inside the cluster. Schedules are database rows; they do40 nothing unless `qcluster` is running.4142## Adding Tasks43441. Put the task function in the app that owns the behavior, usually45 `apps/core/tasks.py` for shared product work.462. Keep the function importable at module import time. Do not rely on request47 objects, local closures, or process-local state.483. Pass durable identifiers such as primary keys, not model instances, open49 files, connections, or large payloads.504. Make the task idempotent. Redis does not give exactly-once execution51 guarantees, and receipt-based brokers can re-run work.525. If the task depends on a just-saved database row, enqueue it from53 `transaction.on_commit(...)`.5455```python56# apps/core/tasks.py57def send_welcome_email(user_id: int) -> None:58 from django.contrib.auth import get_user_model5960 user = get_user_model().objects.get(pk=user_id)61 ...62```6364```python65from django.db import transaction66from django_q.tasks import async_task6768transaction.on_commit(69 lambda: async_task("apps.core.tasks.send_welcome_email", user.pk)70)71```7273Use `q_options` when Django Q2 options would collide with task kwargs:7475```python76async_task(77 "apps.core.tasks.rebuild_report",78 report_id,79 q_options={"timeout": 300, "group": "reports"},80)81```8283## Scheduling Work8485Prefer named, idempotent schedules created by a migration, admin action, or86setup command. Avoid creating schedules unconditionally at import time or app87startup.8889```python90from django_q.models import Schedule9192Schedule.objects.get_or_create(93 name="clear-expired-sessions",94 defaults={95 "func": "django.core.management.call_command",96 "args": "'clearsessions'",97 "schedule_type": Schedule.HOURLY,98 },99)100```101102Use `Schedule.objects.get_or_create(name=..., defaults={...})` when seeding103schedules so repeated setup does not duplicate jobs. Cron schedules require the104optional `croniter` dependency; do not use `Schedule.CRON` unless the generated105project includes it.106107Missed schedules catch up by default. Set `Q_CLUSTER["catch_up"] = False` when108a job should run once after downtime instead of replaying every missed interval.109110## Broker Choices111112### Redis Broker113114This template should normally use Redis:115116```python117Q_CLUSTER = {118 "name": "...",119 "timeout": 3600,120 "workers": 4,121 "redis": REDIS_URL,122}123```124125Redis is fast and matches local Docker, deployment workers, and the default126cache configuration. The default Redis broker does not support delivery127receipts. If a worker host dies catastrophically while executing a task, the128in-flight package can be lost; if task code raises, Django Q2 records a129failure. Use idempotent task design, explicit retries in task code where130needed, and monitoring for failures.131132### ORM Broker133134Use the Django database broker only for low-throughput deployments, local135simplicity, or environments where Redis is unavailable:136137```python138Q_CLUSTER = {139 "name": "...",140 "timeout": 3600,141 "retry": 4800,142 "workers": 4,143 "max_attempts": 2,144 "orm": "default",145}146```147148When switching to ORM:149150- Remove the `"redis"` broker key; configure one broker per cluster unless you151 intentionally use custom clusters.152- Run migrations for `django_q`. If the broker uses a non-default database153 alias, run migrations with `--database <alias>`.154- Increase `"poll"` above the default `0.2` seconds, for example `"poll": 2.0`,155 when you need lower database polling pressure and can tolerate higher queue156 pickup latency.157- The ORM broker enables the Queued Tasks admin table.158- Review Redis-dependent cache, health check, Docker, and deployment settings159 separately. Schedules are always database rows; the broker setting controls160 queued task packages, not the schedule table.161162## Testing163164- Test task business logic by calling the function directly.165- Test enqueueing with synchronous mode:166 - per call: `async_task("apps.core.tasks.fn", arg, sync=True)`167 - per test: override `Q_CLUSTER["sync"] = True`168- For worker/broker integration, run169 `uv run --no-sync python manage.py qcluster` in a separate process and wait170 for `result(task_id, 200)` or a similar bounded wait; do not rely on171 arbitrary sleeps.172- Use `pytest.mark.django_db(transaction=True)` when a real worker process must173 observe committed database rows.174175## Debugging Checklist176177- Is a `qcluster` process running with the same settings module, `SECRET_KEY`,178 broker URL, and cluster name as the web process?179- Can the worker import the dotted task path?180- Did database migrations run, including `django_q` migrations?181- Is Redis reachable from both web and worker containers, or is the ORM broker182 polling the expected database?183- Did a scheduled task duplicate because setup created another `Schedule` row184 with no stable name?185- Did downtime trigger schedule catch-up?186- Is task failure visible in Django admin, logs, or the configured error187 reporter?188189## References190191- Official docs: https://django-q2.readthedocs.io/en/master/192- Upstream repo: https://github.com/django-q2/django-q2