Skip to main content

Supabase Cron Job Monitoring: Catching the Runs That Never Happened

Supabase cron job monitoring has one hard problem: a job that never started leaves no row to query. The documented failure modes, and how to alert on silence.

FLAREWARDEN
FlareWarden Team
7 min read

Your scheduled function was supposed to expire trial accounts every night at 2 a.m. You check cron.job_run_details after a customer complains, and the last fourteen rows are all succeeded. The problem isn’t in those rows. It’s in the four nights that have no row at all, because a job that never started has nothing to report.

Where Supabase Records Your Job Runs

Supabase Cron is a wrapper around the pg_cron Postgres extension. The extension creates a cron schema in your database: jobs live in cron.job, and every run and its status is recorded on cron.job_run_details. The Dashboard reads those same tables under Integrations → Cron.

Querying run history is the right first move when a job is failing:

SELECT jobid, runid, status, return_message, start_time, end_time
FROM cron.job_run_details
WHERE start_time > NOW() - INTERVAL '5 days'
ORDER BY start_time DESC
LIMIT 20;

That query answers “which runs went wrong”. It cannot answer “which runs never happened”, and those are the ones that cost you a customer.

Four Documented Ways a Run Leaves No Trace

All four come from Supabase’s own documentation, current as of September 2026. None of them raise anything you’d notice without looking.

  1. The scheduler worker died. pg_cron runs as a background process inside Postgres. Supabase’s debugging guide has you query pg_stat_activity for an application name of pg_cron scheduler; if the query returns no row, the worker is dead and every schedule stopped with it. Reviving it means a fast reboot from General Settings. Postgres below v15.6.1.122 misses pg_cron v1.6.4+, which added auto-revive.
  2. Too many jobs at once. pg_cron supports up to 32 concurrent jobs, each holding a database connection, and Supabase recommends staying at or below 8 concurrent jobs with each one finishing inside 10 minutes. Past that, jobs collide with the connection ceiling and fail.
  3. The job outran its welcome. Long-running jobs surface as timeout errors, and Supabase’s guidance is to wrap those queries in functions with explicit timeouts rather than let them sprawl.
  4. The project got paused. Supabase’s production checklist states plainly that they may pause Free Plan applications showing low activity over a 7-day period. A paused project runs no schedules. This is the cruellest one, because it usually hits the side-project that’s quietly doing something important.

Notice what these have in common. In three of the four cases the database is the thing that failed, so asking the database whether your job ran is asking the patient to take their own pulse.

The Dashboard Shows You History, Not Absence

Supabase’s Cron interface lets you schedule jobs and monitor runs, and its documentation describes no failure alerting at all. That’s not an oversight so much as a boundary: a scheduler inside your database can tell you what it did, and it cannot tell you what it failed to do while it wasn’t running.

Supabase cron job monitoring therefore has to live somewhere other than Supabase. Detecting absence needs an expectation stored outside the system being watched. Something has to know a run was due at 2:05, notice that nothing arrived by 2:20, and say so. That’s the entire job of heartbeat monitoring, and it’s the same reason the pattern shows up for Vercel cron jobs and Kubernetes CronJobs — different platforms, identical blind spot.

Ping From the End of the Job, Not the Start

The wiring is a single HTTP request from inside the job, and where you put it decides what you actually learn.

A ping at the top of the function proves the job started. A ping at the bottom proves it finished, which is the thing you care about. A function that starts, deletes nothing, fails on an external API call, and exits halfway through still sends the opening ping and leaves you falsely reassured.

For a SQL job, add the ping as the last statement. This uses pg_net, which is not enabled by default — turn it on under Database → Extensions first:

SELECT cron.schedule(
  'expire-trials',
  '0 2 * * *',
  $$
  SELECT expire_trial_accounts();
  SELECT net.http_get(url := 'https://app.flarewarden.com/ping/YOUR-PING-TOKEN');
  $$
);

For an Edge Function, put the fetch after the work completes and inside the success path only:

await expireTrialAccounts()
await fetch(`https://app.flarewarden.com/ping/${PING_TOKEN}`)

The ping token comes from the monitor’s detail page. Four endpoints are available: the bare /ping/{uuid} as a success signal, plus /start, /complete, and /fail when you want the full lifecycle. Use /fail in your error handler and a failing job tells you it failed rather than making you wait out the grace period to infer it.

Size the Grace Period Off Your Worst Run

Grace period is how long the monitor waits past the expected time before calling a run late. The default is 5 minutes, adjustable from 0 to 24 hours.

Set it from your worst observed run, not your average. A nightly job that normally takes 90 seconds but takes nine minutes on the first of the month will page you every month if you sized the grace period off a typical Tuesday. Look at end_time - start_time across your existing history and take the maximum, then add headroom.

Max run duration is the opposite control, and it’s off by default. Turn it on only where an unusually long run is itself the failure — a job holding a lock, a query that lost its index. For most scheduled work, finishing late is not an incident, and switching it on gives you a second way to be woken up for nothing.

Not Every Job Deserves a Page

Instrument all of them and you’ll teach yourself to dismiss the notifications, which is worse than not monitoring at all.

Severity settles this. A cron monitor is critical, degraded, or notify-only, and the choice decides what a miss does to the parent service: critical marks it down, degraded marks it degraded, notify-only sends the message and leaves the status alone. Trial expiry that gates revenue is critical. A materialised view refresh that self-corrects on the next tick is notify-only, if you watch it at all.

The line we’d draw for Supabase specifically: watch the jobs that move money, write data another system reads later, or send customer-facing mail. Everything else can wait for someone to notice, and treating Supabase cron job monitoring as an all-or-nothing exercise is how teams end up with neither. Cron and heartbeat monitoring is on every FlareWarden plan including the free one, so what you watch is a judgment call rather than a billing question. The cron monitor docs cover the ping formats and schedule types in full.


Key Takeaways

  • A missing run leaves no row. cron.job_run_details records what executed; the outage you’re hunting is the gap between rows, which no query against that table can surface.
  • Three of the four documented failure modes take the database with them. A dead scheduler worker, a connection ceiling, and a paused Free Plan project all break the thing you’d query for answers.
  • Free projects pause after 7 days of low activity. Supabase documents this outright, and a paused project runs no schedules at all.
  • Ping at the end, never only at the start. An opening ping proves the function was invoked, which is not the same as proving it did its work.
  • Grace period comes from your worst run. Sizing it off the average guarantees a false alarm the first time month-end arrives.

Want to know the night a Supabase job quietly skipped? Start monitoring free with FlareWarden — cron and heartbeat monitors on every plan, 15 monitors, 5-minute checks, no credit card required.