Skip to content

Django

Requires Django 5.2.17 or newer (below Django 6) on Python 3.11, or Django 6.0.8 or newer with no adapter cap on Python 3.12+. A standalone install can resolve Django 6.1; Celery/all extras currently resolve 6.0.8 because django-celery-beat declares Django<6.1. See the compatibility matrix for the full marker string.

For the new-user onramp see the Django quickstart. This page is the reference.

Pick the engine you use, install with the matching extra. Each extra pulls the engine adapter AND its companion scheduler, so one command covers both the worker and beat processes.

Terminal window
pip install z4j-django[celery] # Celery + celery-beat
pip install z4j-django[rq] # RQ + rq-scheduler
pip install z4j-django[dramatiq] # Dramatiq + APScheduler
pip install z4j-django[huey] # Huey + huey-periodic
pip install z4j-django[arq] # arq + arq-cron
pip install z4j-django[taskiq] # TaskIQ + taskiq-scheduler
pip install z4j-django[all] # every engine (CI / kitchen sink)

pip install z4j-django with no extra installs only the framework adapter. Useful when engine packages are managed elsewhere; otherwise pick an engine extra.

Add to INSTALLED_APPS:

INSTALLED_APPS = [
...,
"django_celery_beat", # if applicable
"z4j_django",
]

The order doesn't matter for z4j (z4j-django auto-imports z4j-celery on module load so the Celery worker_ready signal handler is wired) but django_celery_beat should come before any app whose migrations depend on it.

All settings live under the Z4J dict. Env-var overrides take priority over the dict.

Z4J = {
"brain_url": env("Z4J_BRAIN_URL"), # required
"token": env("Z4J_TOKEN"), # required
"hmac_secret": env("Z4J_HMAC_SECRET"), # required
"project_id": env("Z4J_PROJECT_ID"), # required
"agent_name": env("Z4J_AGENT_NAME", default=None), # optional
}
Key Env var Meaning
brain_url Z4J_BRAIN_URL Base URL of z4j (http://localhost:7700 for local dev; https://... in prod). The agent appends /ws/agent automatically and converts http(s)://ws(s)://.
token Z4J_TOKEN Plaintext bearer token from the dashboard agent-mint dialog. NOT the first-boot setup token.
hmac_secret Z4J_HMAC_SECRET Per-project HMAC secret returned alongside the token in the same mint dialog. The agent refuses to start without it.
project_id Z4J_PROJECT_ID Project slug. z4j takes the authoritative project from the token's record, so any non-empty value works locally; mismatch doesn't fail auth.
Key Env var Default Meaning
agent_name Z4J_AGENT_NAME unset Human label sent in the hello frame's host.name field. Useful for distinguishing multiple workers sharing one token.
environment Z4J_ENVIRONMENT "production" Reserved deployment label available to adapters; the runtime does not automatically add it to every event.
tags (dict only) {} Reserved deployment metadata available to adapters; the runtime does not automatically add it to every event.
dev_mode (dict only) False Allows explicitly configured plain ws:// for non-loopback hosts. Z4J_DEV_MODE from the process environment is deliberately ignored; set settings.Z4J["dev_mode"] in code. Loopback hosts are allowed without this flag.
strict_mode Z4J_STRICT_MODE False Crash on config error instead of degrading gracefully.
autostart Z4J_AUTOSTART True Set False to construct the runtime without starting it (test rigs).
buffer_path (dict only) ~/.z4j/buffer-{pid}.sqlite Explicit code-level path for the on-disk event buffer. Operators should relocate all per-host state with Z4J_HOME; the removed Z4J_BUFFER_PATH variable causes startup refusal. If the default state directory is not writable, the agent falls back to $TMPDIR/z4j-{uid}/buffer-{pid}.sqlite (mode 0700) and logs a warning. See service-user deployments.

The Celery app is auto-detected via 5 candidates (see quickstart §Auto-detect). If your layout is unusual, set:

CELERY_APP = "myproject.celery:app"

That's a top-level Django setting, not part of the Z4J dict.

Web process (runserver / gunicorn / uvicorn)

Section titled “Web process (runserver / gunicorn / uvicorn)”

Z4JDjangoConfig.ready() runs once per worker. It:

  1. Skips if Z4J_DISABLED=true.
  2. Skips if running a one-shot management command (migrate, collectstatic, check, shell, test, etc.).
  3. Skips in the Django autoreload parent process under runserver (only the child with RUN_MAIN=true opens a WebSocket, avoiding a short-lived ghost connection from the supervisor process).
  4. Skips when launched as a Celery sub-command (celery worker, celery beat). The Celery worker process gets its agent from z4j_celery.worker_bootstrap instead, with the engine attached.
  5. Otherwise: builds the runtime, registers with the process-wide singleton, and starts the WebSocket.

Triggered by celery.signals.worker_ready, after Celery has forked its pool. It imports the Celery engine adapter and calls install_agent(engines=[engine]). The engine attaches to Celery's task signals (task_prerun, task_postrun, task_retry, etc.). It is deliberately not started from worker_init, because that signal fires before the fork.

z4j-django eagerly imports z4j_celery at module-load time so the worker_ready signal handler is wired, even though z4j-django itself doesn't start the agent under Celery.

The shutdown hook stops the runtime within a bounded timeout and closes its local buffer and WebSocket. Unsent rows remain in the durable buffer for recovery on a later start; process shutdown does not guarantee network delivery.

In tests, set Z4J_DISABLED=1 in your test runner env to skip startup entirely.

python manage.py z4j_doctor checks the buffer directory and brain DNS / TCP / TLS path, then starts a temporary WebSocket runtime without leaving a persistent agent behind. Use it to diagnose local and network startup failures.

Terminal window
# Always run as the same user the service runs under.
sudo -u www-data /srv/picker/venv/bin/python manage.py z4j_doctor
# Skip the WS round-trip when z4j is intentionally offline:
python manage.py z4j_doctor --no-websocket
# Machine-readable for scripting:
python manage.py z4j_doctor --json

Output:

z4j-doctor (django)
===================
brain_url: https://tasks.example.com/
project_id: picker
buffer_path: /tmp/z4j-33/buffer-7281.sqlite
transport: auto
[OK] buffer_path OK: buffer dir /tmp/z4j-33 is writable
[OK] dns OK: tasks.example.com -> 198.51.100.42
[OK] tcp OK: TCP connect to tasks.example.com:443
[OK] tls OK: TLS TLSv1.3 to tasks.example.com (cert CN='tasks.example.com')
[OK] websocket OK: ws upgrade to https://tasks.example.com/ succeeded
engines auto-detected: celery

Exits 0 if every probe passes, 1 otherwise. It catches the gunicorn-under-www-data startup failure and DNS, firewall, or certificate mismatches. The WebSocket probe returns when the background runtime starts and does not wait for authenticated hello_ack, so confirm token, project, and HMAC authentication in the agent and brain logs. See service-user deployments.

Each gunicorn worker opens one WebSocket. Workers may share one agent token when they should appear as workers beneath the same agent: the runtime gives every process a distinct worker_id, and the brain keys connections by (agent_id, worker_id). Separate tokens create separate agents.

Z4J_AGENT_NAME is a display label, not a connection-identity control. Sharing or separating agents is determined by the minted token; per-process visibility comes from the generated worker IDs.

z4j-django auto-detects ASGI (Daphne / Uvicorn / Hypercorn) and integrates with the existing event loop - no extra configuration.

First, run python manage.py z4j_doctor - it surfaces the most common failures with a specific reason. The list below covers the ones the doctor can't diagnose on its own.

  • Agent never connects - check DJANGO_SETTINGS_MODULE is set, INSTALLED_APPS includes z4j_django, and check startup logs for the [z4j] agent starting line. If the service runs under www-data and the log shows PermissionError: ... /var/www/.z4j, the agent auto-relocates the buffer to $TMPDIR/z4j-{uid} (see service-user deployments).
  • hmac_secret is required - you used the first-boot setup token instead of an agent token. See quickstart troubleshooting.
  • refusing plain ws:// connection - non-loopback host without TLS. Use https:// or set Z4J_DEV_MODE=true for trusted internal networks.
  • connection closed during send: received 4002 - a reconnect displaced the older socket for the same (agent_id, worker_id) slot. One handoff can be normal. A persistent loop means the same worker identity is being registered repeatedly; inspect duplicate startup wiring. Ordinary multi-process workers may share a token because their generated worker IDs differ.
  • Schedule CRUD disabled - requires django-celery-beat with DatabaseScheduler. Filesystem schedulers are read-only.

See engines and z4j and agents for more.