Skip to content

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:

SELECT pgrelay.schedule_job('nightly_report', '0 3 * * *', 'CALL build_nightly_report()');

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.