Surveiller les crons Supabase : attraper le job qui s'est arrêté en silence
Dernière mise à jour : juillet 2026 · Par Yohann Kipfer, Cron-Ping
Supabase planifie de deux façons — pg_cron (SQL, dans la base) et des Edge Functions déclenchées sur un horaire. Aucune ne vous prévient quand elle s'arrête. Un projet Free se met en pause après une semaine d'inactivité, une Edge Function atteint son quota d'invocations mensuel, une migration supprime la fonction qu'un job appelle — et le job ne tourne juste… plus, sans e-mail, sans alarme au dashboard. Transformez le job en dead man's switch en pinguant à la fin :
-- à la fin du corps du job pg_cron
select net.http_get('https://cron-ping.com/p/TOKEN');Si le job tourne, le ping arrive ; s'il s'est arrêté, aucun ping n'arrive dans le délai de grâce et vous recevez un mail ou un Slack. pg_net envoie la requête de façon asynchrone : elle ne bloque et ne casse jamais votre job.
Les deux façons de planifier sur Supabase
Avant de le surveiller, soyez clair sur celle que vous utilisez — elles échouent pour des raisons différentes.
| pg_cron | Edge Function planifiée | |
|---|---|---|
| Où ça vit | la base Postgres | fonction Deno + un trigger pg_cron |
| Défini avec | cron.schedule('nom', '*/5 * * * *', $$…$$) | cron.schedule qui appelle net.http_post sur l'URL de la fonction |
| Exécute | du SQL dans la base | votre TypeScript, invoqué en HTTP |
| Granularité mini | 1 minute (sous la minute avec pg_cron récent) | 1 minute (bornée par pg_cron) |
| Historique | table cron.job_run_details | logs Edge Function + cron.job_run_details |
Les écrans Cron et Integrations du dashboard Supabase sont une interface au-dessus de pg_cron — sous le capot, tout est une ligne dans la table cron.job. C'est important : ça veut dire que vous pouvez inspecter et instrumenter chaque job planifié en simple SQL.
Pourquoi les crons Supabase cassent en silence
- *Un projet Free est mis en pause. Supabase met les projets du plan Free en pause après 7 jours d'inactivité*. La base d'un projet en pause est hors ligne, donc
pg_croncesse totalement de se déclencher — et ça reste en pause jusqu'à une restauration manuelle. Aucun job ne tourne, aucune alerte n'est envoyée. - *Le quota d'Edge Functions est épuisé.* Le plan Free inclut un nombre plafonné d'invocations d'Edge Functions par mois. Une fois dépassé, les invocations sont rejetées — une fonction planifiée qui tourne chaque minute brûle vite un quota, puis s'arrête en silence.
- *Une migration a cassé le job. `pg_cron` stocke le SQL du job en texte. Si une migration ultérieure renomme ou supprime une table ou une fonction référencée, chaque exécution échoue désormais. `pg_cron` ne désactive pas* le job — il continue d'essayer et d'échouer à chaque horaire, et la seule trace est une ligne
faileddanscron.job_run_details. Personne ne surveille cette table. - *Permissions / clés.* Une Edge Function planifiée est en général invoquée avec une clé service-role passée dans le corps de
cron.schedule. Faites tourner cette clé, durcissez la RLS ou changez l'auth de la fonction, et l'appel cron se met à renvoyer 401 — le job « a tourné », l'appel HTTP a échoué, rien n'a eu lieu en aval.
/vercel-cron-monitor et /github-actions-monitoring : un scheduler managé n'a aucune notion d'exécution manquée. Il peut parfois vous dire qu'une exécution a échoué, dans un log que vous ne lisez pas. Il ne peut pas vous dire qu'une exécution qui aurait dû avoir lieu n'a jamais démarré. Seul un heartbeat externe — quelque chose qui attend un ping qui ne vient pas — comble ce trou.Ajouter le ping dans un job pg_cron (pg_net)
pg_net permet à Postgres de faire des appels HTTP. Activez-le une fois, puis ajoutez un ping à la fin du corps du job pour que la requête ne parte qu'après la réussite du vrai travail :
-- une seule fois
create extension if not exists pg_net;
-- planifier un nettoyage de nuit qui signale qu'il a tourné
select cron.schedule(
'nettoyage-nuit',
'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 est asynchrone — il met la requête en file et rend la main tout de suite, donc le ping ne peut jamais ralentir ni casser votre job. Comme c'est la dernière instruction, il n'atteint Cron-Ping que si le delete au-dessus n'a pas levé d'erreur. Si le delete échoue, la transaction est annulée, aucun ping n'est envoyé, et le heartbeat manquant vous alerte.
Pour inspecter l'historique réel d'un job à tout moment :
select jobid, status, return_message, start_time, end_time
from cron.job_run_details
order by start_time desc
limit 10;Ajouter le ping dans une Edge Function planifiée
Si l'horaire déclenche une Edge Function, pinguez depuis l'intérieur, juste avant le return — ainsi un ping veut dire que la fonction a réellement atteint la fin sans planter :
// supabase/functions/rapport-quotidien/index.ts
Deno.serve(async () => {
await construireEtEnvoyerRapport(); // le vrai travail
// heartbeat : atteint seulement si le travail ci-dessus n'a pas planté
await fetch('https://cron-ping.com/p/TOKEN');
return new Response('ok');
});Enveloppez dans un try/catch et tapez l'endpoint /fail dans le catch, pour distinguer une erreur applicative d'un scheduler qui ne s'est jamais déclenché :
Deno.serve(async () => {
try {
await construireEtEnvoyerRapport();
await fetch('https://cron-ping.com/p/TOKEN'); // succès
return new Response('ok');
} catch (e) {
await fetch('https://cron-ping.com/p/TOKEN/fail'); // a tourné, mais échoué
throw e;
}
});Mesurer la durée et attraper une exécution à zéro résultat
Les endpoints /start et /fail transforment le heartbeat en quelque chose de plus riche qu'un simple up/down. Pinguez /start en tête du job et /success à la fin, et Cron-Ping enregistre la *durée* — une agrégation de nuit qui prend normalement 40 secondes et soudain 6 minutes signale une table qui a grossi ou une requête qui a perdu son index.
Vous pouvez aussi rendre le ping conditionnel au résultat. Si un job est censé traiter des lignes et en a traité zéro, c'est souvent un échec déguisé en succès — envoyez /fail :
do $$
declare n int;
begin
delete from sessions_expirees where expire_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
Supabase m'alerte-t-il quand un cron s'arrête ?
Non. Supabase expose un historique dans cron.job_run_details et les logs Edge Functions, mais n'envoie aucune alerte quand un job échoue ou, pire, cesse totalement de tourner (projet Free en pause, quota épuisé). Vous devez soit interroger cette table vous-même, soit pousser un heartbeat vers un moniteur externe.
Que deviennent mes crons quand un projet Supabase Free est mis en pause ?
Ils s'arrêtent complètement. Les projets Free passent en pause après 7 jours d'inactivité ; la base part hors ligne, donc pg_cron ne peut pas se déclencher et tout trigger d'Edge Function planifiée n'a plus rien contre quoi tourner. Le projet reste en pause jusqu'à restauration manuelle. Un heartbeat externe attrape ça dans son délai de grâce — le scheduler interne ne peut pas vous prévenir que sa propre base est à terre.
pg_net va-t-il ralentir ou casser mon cron ?
Non. net.http_get / net.http_post sont asynchrones : ils mettent la requête en file et rendent la main tout de suite, l'appel HTTP réel se fait dans le worker de fond pg_net. Si l'endpoint de ping est lent ou injoignable, votre job SQL a déjà été validé. Mettez le ping en dernière instruction pour qu'il ne s'enfile qu'après la réussite du vrai travail.
pg_cron désactive-t-il un job après une erreur ?
Non — par défaut il continue de lancer le job à l'horaire et enregistre chaque échec dans cron.job_run_details avec un statut « failed ». Un job cassé par une migration échoue donc en silence chaque nuit. C'est exactement le cas qu'une alerte de heartbeat manquant attrape, car le ping d'un job cassé ne quitte jamais la transaction.
Puis-je surveiller une Edge Function planifiée sans toucher au SQL ?
Oui — ajoutez un seul await fetch('https://cron-ping.com/p/TOKEN') avant le return de la fonction. C'est suffisant pour un heartbeat. N'utilisez pg_net que si vous planifiez des jobs SQL purs avec pg_cron, ou si vous voulez rendre le ping conditionnel à ce que le SQL a réellement fait.
Transformez votre cron Supabase en dead man's switch
Un net.http_get à la fin d'un job pg_cron, ou un fetch avant le return d'une Edge Function. Si le job s'arrête — projet en pause, quota explosé, migration qui l'a cassé — le ping n'arrive jamais et Cron-Ping vous alerte par e-mail ou Slack.
Plan gratuit : 10 checks, alertes email, historique 7 jours. Sans carte. Hébergé en UE.