Cron-Ping
AccediInizia gratis

Supabase cron monitoring: catch the job that quietly stopped running

Last updated: July 2026 · By Yohann Kipfer, Cron-Ping

In short

Supabase schedules jobs two ways — pg_cron (SQL, in the database) and Edge Functions triggered on a schedule. Neither tells you when it stops. A free project pauses after a week of inactivity, an Edge Function hits its monthly invocation quota, a migration drops the function a job calls — and the job just… doesn't run, with no email, no dashboard alarm. Turn the job into a dead man's switch by pinging out at the end of it:

-- at the end of the pg_cron job body
select net.http_get('https://cron-ping.com/p/TOKEN');

If the job runs, the ping arrives; if it stopped, no ping arrives inside the grace period and you get an email or Slack message. pg_net sends the request asynchronously, so it never blocks or fails your job.

The two ways Supabase schedules work

Before you can monitor it, be clear which one you're running — they fail for different reasons.

The two Supabase scheduling mechanisms compared
pg_cronScheduled Edge Function
Where it livesthe Postgres databaseDeno function + a pg_cron trigger
Defined withcron.schedule('name', '*/5 * * * *', $$…$$)cron.schedule calling net.http_post to the function URL
RunsSQL inside the DByour TypeScript, invoked over HTTP
Finest granularity1 minute (sub-minute in recent pg_cron)1 minute (bounded by pg_cron)
Run historycron.job_run_details tableEdge Function logs + cron.job_run_details

The Supabase dashboard's Cron and Integrations screens are a UI on top of pg_cron — under the hood everything is a row in the cron.job table. That matters: it means you can inspect and instrument every scheduled job with plain SQL.

Why Supabase cron breaks silently

The common thread with /vercel-cron-monitor and /github-actions-monitoring: a managed scheduler has no concept of a missed run. It can tell you a run failed (sometimes, in a log you're not reading). It cannot tell you a run that should have happened never started. Only an external heartbeat — something waiting for a ping that doesn't come — closes that gap.

Add the ping in a pg_cron job (pg_net)

pg_net lets Postgres make HTTP calls. Enable it once, then append a ping to the end of the job body so the request only goes out after the real work succeeds:

-- one-time
create extension if not exists pg_net;

-- schedule a nightly cleanup that reports it ran
select cron.schedule(
  'nightly-cleanup',
  '0 3 * * *',
  $$
    delete from events where created_at < now() - interval '30 days';
    select net.http_get('https://cron-ping.com/p/TOKEN');
  $$
);

net.http_get is asynchronous — it queues the request and returns immediately, so the ping can never slow down or break your job. Because it's the last statement, it only reaches Cron-Ping if the delete above it didn't throw. If the delete errors, the transaction rolls back, no ping is sent, and the missed heartbeat alerts you.

To inspect a job's real history at any time:

select jobid, status, return_message, start_time, end_time
from cron.job_run_details
order by start_time desc
limit 10;

Add the ping in a scheduled Edge Function

If the schedule triggers an Edge Function, ping from inside the function, just before it returns — so a ping means the function actually reached the end without throwing:

// supabase/functions/daily-report/index.ts
Deno.serve(async () => {
  await buildAndSendReport();          // the real work

  // heartbeat: only reached if the work above didn't throw
  await fetch('https://cron-ping.com/p/TOKEN');

  return new Response('ok');
});

Wrap it in try/catch and hit the /fail endpoint in the catch, so an application error is distinguished from a scheduler that never fired:

Deno.serve(async () => {
  try {
    await buildAndSendReport();
    await fetch('https://cron-ping.com/p/TOKEN');        // success
    return new Response('ok');
  } catch (e) {
    await fetch('https://cron-ping.com/p/TOKEN/fail');    // ran, but failed
    throw e;
  }
});

Measure duration and catch a zero-result run

The /start and /fail endpoints turn the heartbeat into something richer than up/down. Ping /start at the top of the job and /success at the end, and Cron-Ping records the *duration* — a nightly aggregation that normally takes 40 seconds and suddenly takes 6 minutes is a signal a table grew unexpectedly or a query lost its index.

You can also make the ping conditional on the result. If a job is supposed to process rows and processed zero, that's often a failure disguised as a success — send /fail instead:

do $$
declare n int;
begin
  delete from expired_sessions where expires_at < now();
  get diagnostics n = row_count;
  if n = 0 then
    perform net.http_get('https://cron-ping.com/p/TOKEN/fail');
  else
    perform net.http_get('https://cron-ping.com/p/TOKEN');
  end if;
end $$;

FAQ

Does Supabase alert me when a cron job stops?

No. Supabase surfaces a run history in cron.job_run_details and Edge Function logs, but it sends no alert when a job fails or, worse, stops running entirely (a paused free project, an exhausted quota). You have to either poll that table yourself or push a heartbeat out to an external monitor.

What happens to my cron jobs when a free Supabase project is paused?

They stop completely. Free projects pause after 7 days of inactivity; the database goes offline, so pg_cron can't fire and any scheduled Edge Function trigger has nothing to run against. The project stays paused until you restore it manually. An external heartbeat catches this within its grace period — the internal scheduler can't warn you about its own database being down.

Will pg_net slow down or break my cron job?

No. net.http_get / net.http_post are asynchronous: they enqueue the request and return immediately, and the actual HTTP call happens in the pg_net background worker. If the ping endpoint is slow or unreachable, your SQL job has already committed. Put the ping as the last statement so it only enqueues after the real work succeeds.

Does pg_cron disable a job after it errors?

No — by default it keeps running the job on schedule and records each failure in cron.job_run_details with a status of 'failed'. So a job broken by a migration fails silently every night. That's exactly the case a missed-heartbeat alert catches, because a broken job's ping never leaves the transaction.

Can I monitor a scheduled Edge Function without touching SQL?

Yes — add a single await fetch('https://cron-ping.com/p/TOKEN') before the function returns. That's enough for a heartbeat. Use pg_net only if you schedule pure-SQL jobs with pg_cron, or if you want the ping to be conditional on what the SQL actually did.

Turn your Supabase cron into a dead man's switch

One net.http_get at the end of a pg_cron job, or one fetch before an Edge Function returns. If the job stops — paused project, blown quota, a migration that broke it — the ping never arrives and Cron-Ping emails or Slacks you.

Create a free check

Free plan: 10 checks, email alerts, 7-day history. No card. EU-hosted.

Related

Monitoring Vercel Cron JobsThe same silent-failure problem on Vercel — a ping from inside the route handler.Monitoring the Laravel schedulerOne master cron dispatches every task — and hides every silent failure.Alert when a backup stops runningThe dead man's switch on the job you only notice when it's too late.Cron-Ping documentationStart, success and fail pings; grace periods; snippets for HTTP calls.

Sources