Skip to content
LastPing
GUIDE · SUPABASE · PG_CRON

Supabase Cron and pg_cron not running: what to check, and how to get told

Supabase Cron runs on pg_cron, inside your database. When its scheduler process dies or a job fails, the evidence sits in tables nobody is watching. Check the three queries and the causes below, then end each job with a net.http_get to LastPing, so a run that does not happen opens an incident.

Your assistant can do this for you

Find out why my Supabase cron job is not running, and monitor it with LastPing

Supabase's debugging guide: if the pg_cron scheduler query returns no row, "the worker has died. To revive it, you must go to the General Settings and initiate a fast reboot." (Supabase, pg_cron debugging guide.)
On this page
  1. Check these first: three queries
  2. Eight reasons a Supabase cron job does not run
  3. Get alerted when a job does not run
  4. What this catches
  5. Questions people ask

Check these first: three queries

The scheduler, its jobs and their history all live in the database, so start there.

-- Is the scheduler process alive?
select pid from pg_stat_activity where application_name ilike 'pg_cron scheduler';

-- Which jobs exist, and are they active?
select jobid, jobname, schedule, active from cron.job;

-- What happened on the last runs?
select jobid, status, return_message, start_time, end_time
from cron.job_run_details order by start_time desc limit 20;

cron.job_run_details "grows with every Job run and is never cleaned up automatically, even after a Job is unscheduled", so Supabase suggests a daily job that deletes rows older than 7 days.

Eight reasons a Supabase cron job does not run

In the order worth checking, quoting the Supabase and pg_cron documentation where it is exact.

The scheduler process has died

pg_cron runs as a process inside Postgres. If this query returns no row, Supabase's debugging guide says "the worker has died", and the fix is a fast reboot from the project's General Settings:

select pid, backend_start, state
from pg_stat_activity
where application_name ilike 'pg_cron scheduler';

Supabase recommends Postgres v15.6.1.122 or later, which brings pg_cron 1.6.4 with "auto-revive capabilities".

The job is inactive

Every job has an active flag, and cron.alter_job(<jobid>, active := false) switches a job off without deleting it. select jobid, jobname, schedule, active from cron.job; shows them all.

The schedule is in GMT

pg_cron's cron.timezone setting defaults to GMT, and Supabase's own examples are written in GMT: 30 3 * * 6 is Saturday at 03:30 GMT. Paste the expression into the cron expression explainer to see those runs in your own zone.

The job failed

Each run and its status is recorded in cron.job_run_details. Supabase's debugging query lists the runs that neither succeeded nor are still running; return_message holds the error. Long-running jobs may show timeout errors there: Supabase suggests wrapping such a query in a function with its own timeout.

select jobid, status, return_message, start_time
from cron.job_run_details
where status <> 'succeeded' and status <> 'running'
  and start_time > now() - interval '5 days'
order by start_time desc
limit 10;

Too many jobs at once

pg_cron supports up to 32 concurrent jobs, each using a database connection (cron.max_running_jobs, default 32). Supabase goes further: "For best performance, we recommend no more than 8 Jobs run concurrently", and "Each Job should run no more than 10 minutes." Space jobs out rather than starting them all on the hour.

The previous run is still going

pg_cron runs only one instance of a job at a time: "If a second instance is triggered before the first finishes, it's queued and starts as soon as the first one completes." A job that takes longer than its interval falls further behind on every run.

A seconds schedule on an older Postgres

pg_cron accepts intervals such as '30 seconds' (1 to 59 seconds, not combined with other units). On Supabase, seconds need Postgres 15.1.1.61 or later.

The database is a standby

From the pg_cron documentation: it "does not run any jobs as long as a server is in hot standby mode, but it automatically starts when the server is promoted." Jobs belong on the primary.

Get alerted when a job does not run

The scheduler lives inside the database, so when it stops, nothing inside the database can say so. A ping from the job, checked against the schedule outside it, can.

Create a monitor with the job's schedule

In the console, create a monitor with the job's cron expression in UTC (GMT), plus a grace window. Copy the ping URL.

Ping from the job with pg_net

Supabase documents calling HTTP from a cron job with pg_net. Put the work and the ping in one function, ping last:

create or replace function nightly_cleanup()
returns void language plpgsql as $$
begin
  delete from events where event_time < now() - interval '1 week';
  perform net.http_get('https://ping.lastping.dev/<your-monitor-id>');
end;
$$;

select cron.schedule('nightly-cleanup', '0 3 * * *', 'select nightly_cleanup()');

pg_net's documentation is what makes this safe: "HTTP requests are not started until the transaction is committed." If the delete fails, the transaction rolls back and the ping is never sent, so a failed run looks like a missed one and opens an incident after the grace window.

Optional: report the error at once

To be alerted immediately with the error text, catch it and post it to /fail. The catch means the run is recorded as succeeded in cron.job_run_details, so LastPing becomes the place the failure shows:

create or replace function nightly_cleanup()
returns void language plpgsql as $$
begin
  begin
    delete from events where event_time < now() - interval '1 week';
  exception when others then
    perform net.http_post(
      url  := 'https://ping.lastping.dev/<your-monitor-id>/fail',
      body := jsonb_build_object('error', SQLERRM)
    );
    return;
  end;
  perform net.http_get('https://ping.lastping.dev/<your-monitor-id>');
end;
$$;

pg_net keeps each response for 6 hours in net._http_response, so select status_code, error_msg, created from net._http_response order by created desc limit 5; shows whether the pings got through. Its requests time out after 2,000 ms by default.

What this catches

The job reports what it can; the missing ping reports the rest.

  • The scheduler stopped: a dead pg_cron process, a database that is down or a standby that was never promoted. No ping in the window opens an incident.
  • The job was switched off: an inactive job, or one removed by a migration, sends nothing, and the silence raises the alert.
  • The job failed: the rolled-back transaction never sends its ping, so the incident opens after the grace window, or at once with the optional /fail variant.
  • The job fell behind: a run queued behind a slow previous run pings late; past the grace window, that is an incident too.
  • Recovery: the next successful run closes the incident and sends a recovery notice.

The same pattern works for pg_cron outside Supabase: the pg_cron documentation covers loading it through shared_preload_libraries and choosing its database with cron.database_name.

Questions people ask

Front-loaded answers: the most important fact first.

  • Why is my pg_cron job not running?

    Check that the pg_cron scheduler process is alive in pg_stat_activity, that the job is active in cron.job, and what cron.job_run_details recorded for its last runs. Remember the schedule is in GMT by default, that a job never runs twice at once, and that pg_cron runs no jobs on a hot standby.

  • Where can I see Supabase cron job history?

    In the cron.job_run_details table: each run with its status, return_message, start_time and end_time. It is never cleaned up automatically, so schedule a job that deletes old rows.

  • What timezone does Supabase Cron use?

    GMT. pg_cron's cron.timezone setting defaults to GMT, and Supabase's examples are written in GMT.

  • How many cron jobs can run at the same time?

    pg_cron supports up to 32 concurrent jobs, each using a database connection. Supabase recommends no more than 8 running concurrently, and no job running longer than 10 minutes.

  • Is a pg_net request sent if the cron job fails?

    No. pg_net does not start HTTP requests until the transaction is committed, so a job that fails and rolls back never sends its ping. That is why a ping at the end of the job is a reliable success signal.

  • How do I get alerted when a Supabase cron job does not run?

    Create a LastPing monitor with the job's schedule in GMT and a grace window, and end the job's function with perform net.http_get to the monitor URL. A missed or failed run sends no ping and opens an incident.

  • Is this monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

The scheduler inside your database can't report its own death.

One ping at the end of the job now beats finding a week of stale rows later. First alert wired up in minutes, and LastPing monitors itself the same way.