Cron Jobs¶
Run tasks on a recurring cadence. Pick one scheduler — application-side
beat, or Postgres-side pg_cron. Both
read the same SCHEDULE. Installing the pg_cron app makes absurd_beat and
absurd_worker --beat raise CommandError.
Declare a schedule¶
TASKS = {
"default": {
"BACKEND": "django_absurd.backends.AbsurdBackend",
"OPTIONS": {
"SCHEDULE": {
"nightly-report": {
"task": "myapp.tasks.send_report", # dotted path to a @task
"cron": "0 2 * * *", # 2am daily
},
"heartbeat": {
"task": "myapp.tasks.ping",
"cron": "*/5 * * * *",
"queue": "monitoring", # optional; must be declared
"kwargs": {"source": "beat"}, # optional
},
},
},
},
}
| Key | Required | Description |
|---|---|---|
task |
yes | Dotted import path to a @task function. |
cron |
yes | Cron expression — grammar differs per scheduler, see below. |
queue |
no | Queue to enqueue on; defaults to the backend's. Must be declared. |
args / kwargs |
no | Passed to the task each firing. args a JSON array, kwargs a JSON object. |
Validated by manage.py check (absurd.E007); names are limited to [A-Za-z0-9_-].
Application-side (beat)¶
Beat enqueues each task when its slot comes due; a worker then runs it like any other.
- Run exactly one. No leader election — concurrent beats each fire every slot.
- Never backfills. A slot missed while down is skipped.
- A failed enqueue is not retried either. Beat logs the error and moves to the next slot, so an unprovisioned target queue costs you every slot until someone provisions it.
- Grammar is croniter: 5-field, or 6-field with a
leading seconds column (
"*/30 * * * * *"). - Expressions use Django's
TIME_ZONE.
Runnable demo:
examples/beat/.
Postgres-side (pg_cron)¶
INSTALLED_APPS = [
# ...
"django_absurd",
"django_absurd.pg_cron", # must come AFTER "django_absurd"
]
Postgres fires the schedule directly — no beat process. migrate reconciles SCHEDULE
into pg_cron jobs and your existing
workers run the tasks. A settings-only change needs no new migration, so
"migrate on deploy" covers it. The extension itself is one-time
operator setup.
- Grammar is pg_cron's own: 5-field cron,
<n> seconds(1–59) for sub-minute cadence, or an@daily-style alias. Validated bymanage.py check. - Beat's 6-field form and
#are refused, though pg_cron accepts them: its parser reads five fields and takes the rest as the command, so"*/30 * * * * *"would schedule"*/30 * * * *"— a cadence you didn't write, reported as valid. - Timezone is the
cron.timezoneGUC (Grand Unified Configuration — Postgres's term for a server setting), default GMT — not Django'sTIME_ZONE. Set it to match if yours is non-UTC. - To stop a job, remove it from
SCHEDULE. Every reconcile re-arms settings-owned jobs, so disabling one directly incron.jobdoesn't survive the next deploy. absurd.W003if the app is ordered before"django_absurd".
Runnable demo:
examples/pg_cron/.
How jobs reach your database
pg_cron is cluster-wide: only the database named by
cron.database_name may
hold the extension, and yours probably isn't it. django-absurd finds that database
itself and schedules each job
cross-database —
nothing to configure either way.
Reconcile without migrating¶
For pipelines that skip migrate when no migration files changed. Reports synced/pruned
counts, non-zero exit on error.
- Always connect as the same role. pg_cron keys jobs on
(jobname, username)and runs each as its scheduling role, so mixing roles duplicates jobs and breaks pruning.
Author schedules in the admin¶
Admins author their own schedules alongside the settings-declared ones — see Admin.
Test databases¶
Every cron.* write is inert on a test database, detected automatically — otherwise a
leftover schedule would fire for real against test data.
| Option | Default | Effect |
|---|---|---|
PG_CRON_ON_TEST_DB |
False |
The opt-in. Without it writes no-op and absurd_sync_crons refuses to run. |
SYNC_SCHEDULES_ON_MIGRATE |
True |
migrate's automatic reconcile against a real database. |
SYNC_SCHEDULES_ON_TEST_DB |
False |
Same, against a test database. Setting it without PG_CRON_ON_TEST_DB is absurd.E011. |
→ Testing — getting a SCHEDULE into pg_cron.
Uninstall¶
Run this before removing "django_absurd.pg_cron" from INSTALLED_APPS or
switching back to beat. Removing the app stops the reconcile but leaves the jobs firing,
and nothing cleans up afterwards.
- It unschedules every owned job and deletes its row, admin-authored included —
otherwise the next
migrateresurrects them. Hence the confirmation prompt. migratealone never tears down admin-authored jobs, so it is not a substitute.
Operator setup¶
One-time, on the central database named by cron.database_name — not necessarily
the Absurd one. See pg_cron's own docs.
- pg_cron ≥ 1.4 — reconciles call
cron.schedule_in_databaseandcron.alter_job. shared_preload_libraries = pg_cron— needs a server restart.CREATE EXTENSION pg_cron.- Grants, unless the scheduling role owns the extension.
alter_jobis not optional —schedule_in_databaseonly appliesactiveon first create:
GRANT CONNECT ON DATABASE <central_database> TO <scheduling_role>;
GRANT USAGE ON SCHEMA cron TO <scheduling_role>;
GRANT EXECUTE ON FUNCTION
cron.schedule_in_database(text, text, text, text, text, boolean)
TO <scheduling_role>;
GRANT EXECUTE ON FUNCTION
cron.alter_job(bigint, text, text, text, text, boolean)
TO <scheduling_role>;
GRANT EXECUTE ON FUNCTION cron.unschedule(bigint) TO <scheduling_role>;
CONNECT applies even though none of your tables live there — the central connection
reuses the app's own credentials and swaps only the database name.
- A scheduling role that isn't a superuser needs one grant more, or
migratefails:
Finding the central database means reading the cron.database_name GUC, which pg_cron
marks superuser-only. Without it: permission denied to examine "cron.database_name".
Not specific to managed Postgres — it applies on any server; local setups miss it only
because they connect as postgres.
Managed Postgres (RDS, Cloud SQL, Azure) exposes these as parameter-group flags.
manage.py check reports absurd.E012 if the central database is unreachable or
missing the extension. It stays quiet under a test suite unless PG_CRON_ON_TEST_DB
opts that database in.
Also worth scheduling: a
cron.job_run_details
purge. It's the only place fire-time failures show up, and it grows unbounded.
Docker¶
The stock postgres image ships no pg_cron. Copy django-absurd's own — its pg_cron
suite runs against exactly these:
Dockerfile.pg_cron— Debian base (Alpine has no pg_cron package), the PGDG package, and an initdb script that creates the extension on the central database.- The
db_pg_cronservice incompose.yaml— theshared_preload_librariesandcron.database_nameserver flags, which can't live in the image.