How to monitor Supabase pg_cron jobs

A pg_cron job record can exist while the SQL statement repeatedly fails. Verify the database operation before pinging.

Report success and failure

Create a check and set PING_URL to its private URL. Replace the example command with your actual task. These examples preserve command failure and do not let a failed reporting request turn a successful job into a failure.

#!/bin/sh
# Poll terminal results of an existing pg_cron job, not a second copy of its SQL.
# Set PG* connection variables, PING_URL, CRON_JOB_ID and STATE_FILE securely.
: "${PING_URL:?}" "${CRON_JOB_ID:?}" "${STATE_FILE:?}"
case "$CRON_JOB_ID" in ''|*[!0-9]*) exit 2 ;; esac
result=$(psql -X -At -F '|' -v ON_ERROR_STOP=1 -c \
  "SELECT runid,status FROM cron.job_run_details WHERE jobid=$CRON_JOB_ID AND status IN ('succeeded','failed') ORDER BY runid DESC LIMIT 1") || exit 1
[ -n "$result" ] || exit 0
rid=${result%%|*}
status=${result#*|}
previous=$(cat "$STATE_FILE" 2>/dev/null || :)
[ "$previous" != "$rid" ] || exit 0
code=1
[ "$status" = succeeded ] && code=0
curl -fsS -m 10 --retry 3 "$PING_URL/$code?rid=$rid" >/dev/null || exit 1
printf '%s\n' "$rid" > "$STATE_FILE"

Connect it to Supabase pg_cron

Keep your existing pg_cron job. Save the poller as /opt/poll-pg-cron.sh on an independent Unix host with psql and curl. Set its numeric job ID and a private writable STATE_FILE. Run the poller every minute; set the CronPulse check to the cadence of the database job, allowing at least two minutes of polling grace.

-- Existing database job (replace the SQL with your own operation):
SELECT cron.schedule('reports', '*/5 * * * *',
  $$CALL public.refresh_reports();$$);
-- Record the returned job ID as CRON_JOB_ID on the polling host.
-- On that separate host, with credentials loaded by a protected wrapper:
-- * * * * * /bin/sh /opt/poll-pg-cron.sh

These are integration fragments for an existing project, not a full application. Set the URL in the job’s environment, keep credentials private and ensure the runtime has the dependencies shown.

Things to watch

The poller reports only the latest terminal run and persists its run ID locally. If multiple runs finish between polls, intermediate outcomes are not reported. Enable pg_cron run logging and use a database role allowed to read job_run_details. Do not delete the state file between polls. Failed SQL maps to exit code 1; succeeded SQL maps to 0. A polling outage is itself caught by a missing heartbeat. This avoids asynchronous HTTP requests being rolled back with a failed database transaction.

The shell examples wait one second between start and work to respect the per-check rate guard. For very short tasks, completion-only pings avoid that delay. A killed process may never send a failure ping; the completion deadline catches the missing run instead.

Test the setup

  1. Run a successful task and confirm the check turns up.
  2. Force a nonzero exit or exception and verify the check turns down.
  3. Stop the scheduler after one success and wait for the expected run plus grace.

Set the schedule to your task’s completion cadence, allowing for runtime and queue delay. Use the cron helper to review your calendar schedule.

Questions about Supabase pg_cron

Where should the heartbeat run?
Inside the completed task, after the important work. A pg_cron job record can exist while the SQL statement repeatedly fails. Verify the database operation before pinging.
Which grace period should I use?
Allow for job runtime and scheduler startup delay. CronPulse evaluates deadlines once a minute, so alerts generally follow within about 1–2 minutes after grace ends.
Does this test the result of my work?
No. A success ping proves your command reached the reporting step. Validate output and, for backups, test restoration. Missing completion pings catch stopped or killed jobs.

Platform reference: official Supabase pg_cron documentation. Adapt fragments to your installed version.

Give your next job a heartbeat.

20 checks free. No card. Built in Ireland.

Create your first check ↗