Replacing pg_cron¶
Who needs this book
Anyone who wants SQL — or one of their channels — to run on a schedule: nightly, every ten seconds, the second Tuesday of the month. If you use pg_cron today, this book shows the one-to-one replacement. If every event you send is triggered by something happening in the database, you can skip it.
Since v1.4, pg_relay schedules recurring jobs natively. Any registered channel can fire on a schedule — every night at 03:00, every 10 seconds, the 2nd Tuesday of the month — driven by the same 1-second Processor poll, the same durable queue, and the same audit log as every other pg_relay event. If you run pg_cron today, everything it does maps onto one or two pg_relay calls, and several things it can't do come along for free.
A complete scheduled job is one statement, in pg_cron's own parameter shape — name, schedule, command:
From that moment the Processor runs that SQL at every occurrence, records
every run in the audit log under one stable job id, and keeps doing so across
restarts, reconnects, and failovers — the job is a durable row in
pgrelay.queue, not a timer in anyone's memory. (The SQL runs on a seeded,
reserved channel, pg_relay_adhoc, that only schedule_job() and its
one-shot sibling run_sql() can reach —
Creating and Changing Jobs has the details, including
the second form that fires one of your channels with a payload instead.)
Why replace pg_cron?¶
Second-level scheduling. pg_cron's floor is one minute. pg_relay accepts a
6-field cron form whose first field is seconds ('*/10 * * * * *' — every
ten seconds), and plain intervals down to seconds ('30 seconds'). The
Processor polls every second, so that resolution is real.
No extra infrastructure. pg_cron is another extension, a
shared_preload_libraries entry, a server restart, and a set of background
workers — and on managed cloud platforms it's available only where the
provider chose to allow it, often pinned to one database. pg_relay's scheduler
is plain SQL inside the extension you already have; the Processor you already
run is the clock. Nothing to preload, nothing to restart, and it works
anywhere pg_relay works — including cloud platforms
where the extension is installed by script.
One queue, one audit trail. A scheduled run is an ordinary pg_relay event:
it appears in pgrelay.log with a status, an error message, and a duration,
under the job's stable id — pgrelay.scheduled_job_runs() reads it back just
like pg_cron's cron.job_run_details. Ad-hoc events and scheduled jobs share
one operational surface instead of two.
Jobs never overlap themselves. While a run is executing, the next occurrence cannot start — the guarantee is structural (one row, locked while running), not something you must build with advisory locks in your job SQL, as pg_cron users often do.
Richer calendar rules. Beyond cron, a job's schedule can be a
pgrelay.schedule value — "the 2nd Tuesday
at 09:00," "the last day of the month," "Monday 05:00 but Tuesday 17:30 and
20:00" — rules cron simply cannot express.
Operational control per job. pause_job() suspends one job (with an
optional automatic resume time), run_job_now() fires an immediate extra run
for testing, and the whole fleet can still be paused with the existing
fleet controls. pg_cron has no
equivalent of either.
High availability included. Under pg_relay's multi-master modes, an unrestricted scheduled job survives the death of the node it was created on — a live node adopts it. pg_cron runs on one server, full stop.
How the two compare¶
| pg_cron | pg_relay scheduled jobs | |
|---|---|---|
| Finest resolution | 1 minute (seconds via a workaround interval syntax) | 1 second |
| Schedule forms | 5-field cron, interval | 5-field cron, 6-field cron with seconds, interval, pgrelay.schedule calendar rules |
| Installation | Extension + shared_preload_libraries + restart |
Already in pg_relay ≥ 1.4 |
| Where jobs run | The database server's background workers | The Processor host(s) you already run |
| Job identity | jobid / jobname |
Stable queue row id / schedule_name |
| List jobs | cron.job |
pgrelay.list_scheduled_jobs() |
| Run history | cron.job_run_details |
pgrelay.scheduled_job_runs() + pgrelay.log |
| Overlap protection | Build it yourself | Guaranteed — a job never overlaps itself (a detached job's late finish skips the next occurrence, audited) |
VACUUM / non-transactional commands |
Yes | Yes — ad-hoc SQL runs outside a transaction; use the detached lane for long maintenance |
| Runs as | The scheduling user | The pgrelay role, or p_run_as — a role the scheduler holds, opted in by the DBA |
| Per-job timeout | — | p_timeout_seconds (inline default 300 s, cap 12 h) |
| Missed occurrences | Skipped | Skipped (same behaviour) |
| Pause one job | Deactivate (cron.alter_job) |
pause_job() / resume_job() — bounded pauses self-resume |
| Run now | — | run_job_now() |
| Survives node failure | No | Yes (multi-master modes) |
Who needs this book
Anyone running recurring database work — maintenance, reports, refreshes,
recurring notifications — with pg_relay ≥ 1.4. If you only need a
one-off deferred event, that's just pgrelay.notify(..., p_run_at := ...),
covered in Sending Events. If you're
migrating from pg_cron, the cookbook at the end of
How It Works maps each pg_cron call to its
pg_relay equivalent.
The pieces, at a glance¶
| Function | What it does |
|---|---|
schedule_job(name, schedule, sql/payload, ...) |
Create a job — or change it, same call, same name |
unschedule_job(name) |
Withdraw a job for good |
pause_job(name [, until]) |
Suspend a job; bounded pauses resume themselves |
resume_job(name) |
Lift a pause now |
run_job_now(name) |
Fire an immediate extra run |
list_scheduled_jobs() |
Every live job: next run, stats, paused state |
scheduled_job_runs([name] [, limit]) |
Per-run history, newest first |
_next_run(spec [, after]) |
Preview and validate a schedule spec |
All of them are management-grade: grant them with pgrelay.grant_user(role)
(see Granting Permissions).
The Processor itself needs no new grants — a v1.3 Processor picks up v1.4
scheduling with no operator action at all.
Continue to Schedule Syntax.