Cron-Ping

Surveiller le scheduler Laravel : le cron unique qui masque tous les échecs silencieux

Dernière mise à jour : juillet 2026 · Par Yohann Kipfer, Cron-Ping

En résumé

Le scheduler Laravel tourne sur une seule ligne de crontab — * * * * * php artisan schedule:run. Chaque tâche ->daily(), ->hourly() ou ->everyFiveMinutes() vit dans ce processus unique. Si cette ligne est retirée, que le conteneur est tué par l'OOM ou qu'un déploiement efface la crontab, plus rien ne tourne — et Laravel ne lève aucune erreur, puisque de son point de vue il n'a simplement jamais été appelé. Mettez un heartbeat dans le scheduler :

// routes/console.php (Laravel 11+)
Schedule::call(fn () => Http::get('https://cron-ping.com/p/TOKEN'))
    ->everyFiveMinutes()
    ->name('cron-ping-heartbeat');

Le ping ne part que si le scheduler est vivant. Aucun ping dans le délai de grâce → vous recevez un mail ou un message Slack. C'est la seule chose que php artisan schedule:list ne dira jamais : il affiche ce qui devrait tourner, pas ce qui a tourné.

Pourquoi `schedule:run` est un point de défaillance unique — et silencieux

L'installation officielle Laravel, c'est une seule entrée de crontab sur le serveur, ajoutée une fois puis oubliée :

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Cette ligne réveille Laravel chaque minute. Laravel lit votre planning, détermine quelles tâches sont dues cette minute, et les lance. Une centaine de tâches planifiées — sauvegardes, facturation, réchauffage de cache, e-mails de rapport — dépendent donc de ce *seul* processus appelé. Le scheduler est un répartiteur, pas un démon : il n'a aucune mémoire des minutes ratées et aucune reprise.

Regardez la redirection : >> /dev/null 2>&1. Elle figure dans la doc officielle de Laravel, et elle jette toute la sortie. C'est volontaire — sinon cron vous envoie un mail chaque minute — mais ça veut aussi dire que quand schedule:run se met à échouer (une erreur fatale dans un service provider, un .env cassé, une base indisponible au boot), l'erreur part dans /dev/null. La liste des tâches reste parfaite. Rien ne tourne.

Les pannes qui tuent tout le scheduler d'un coup : la ligne de crontab jamais re-ajoutée après une reconstruction de serveur ; un outil de déploiement qui réécrit la crontab et la perd ; le conteneur Docker/Kubernetes qui fait tourner schedule:work tué par l'OOM sans redémarrage ; une fatale PHP avant même que Laravel démarre. Dans tous les cas, schedule:list affiche encore vos tâches, et chacune est morte.

Étape 1 — un heartbeat global dans le scheduler

Le signal le plus fiable est un ping émis par le scheduler lui-même, pour qu'il n'arrive que si le répartiteur tourne réellement. Sur Laravel 11 et 12, le planning vit dans routes/console.php :

// routes/console.php  (Laravel 11 / 12)
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schedule;

Schedule::call(fn () => Http::get('https://cron-ping.com/p/TOKEN'))
    ->everyFiveMinutes()
    ->name('cron-ping-heartbeat')
    ->withoutOverlapping();

Sur Laravel 10 et avant, la même chose va dans la méthode schedule() de app/Console/Kernel.php avec $schedule->call(...). Créez le check avec une *période de 5 minutes* et une grâce courte (2 minutes couvrent une file lente). Deux pings consécutifs manqués = le scheduler est à terre, pas une tâche, toutes.

Pourquoi un call() et pas un curl autour de schedule:run dans la crontab ? Parce qu'un curl au niveau crontab part même quand Laravel plante au boot — il rapporterait vert pendant que toutes les tâches sont mortes. Le ping doit venir d'après le démarrage réussi de Laravel, quand le répartiteur boucle vraiment.

Étape 2 — surveillance par tâche avec les hooks natifs

Le heartbeat prouve que le scheduler tourne. Il ne prouve pas que votre sauvegarde de nuit, elle, a réussi. Le scheduler Laravel embarque des *hooks de ping URL* faits exactement pour ça — aucun package, ils utilisent Guzzle dont Laravel dépend déjà :

Schedule::command('backup:run')
    ->dailyAt('02:30')
    ->withoutOverlapping()
    ->pingOnSuccess('https://cron-ping.com/p/BACKUP_TOKEN')
    ->pingOnFailure('https://cron-ping.com/p/BACKUP_TOKEN/fail');

La liste complète des hooks, directement issue de Illuminate\Console\Scheduling :

Les hooks de tâche du scheduler Laravel et leur rôle
HookSe déclenchePour
->pingBefore($url)juste avant le début de la tâcheun ping /start pour mesurer la durée
->thenPing($url)après la tâche (quel que soit le résultat)un simple heartbeat de succès
->pingOnSuccess($url)seulement si la tâche sort en 0confirmer que le job a vraiment marché
->pingOnFailure($url)seulement si la tâche sort non-zérotaper l'endpoint /fail
->pingBeforeIf($cond,$url)avant, si la condition est vraiene pinguer que sur la prod
->before() / ->after()exécuter une closure autour de la tâchelog / métriques maison
->onSuccess() / ->onFailure()closure selon le résultatnotifier un canal précis

Associez-les aux trois endpoints Cron-Ping : ->pingBefore() sur /p/TOKEN/start (démarre le chrono), ->pingOnSuccess() sur /p/TOKEN (l'arrête, enregistre l'exécution), ->pingOnFailure() sur /p/TOKEN/fail (alerte même si le scheduler, lui, va bien). Une sauvegarde qui tourne mais échoue à mi-chemin est enfin détectée — le heartbeat seul n'aurait rien vu.

Étape 3 — attraper un scheduler mort au dernier déploiement

La façon la plus fréquente de tuer un scheduler Laravel, c'est un déploiement, et chaque hébergement échoue différemment :

Un heartbeat avec une grâce serrée transforme ces trois cas en e-mail dans les minutes qui suivent le déploiement, au lieu d'une découverte plusieurs jours après quand quelqu'un demande pourquoi le rapport du lundi n'est jamais arrivé.

Les pièges : `withoutOverlapping`, tâches sous la minute, et la file

`withoutOverlapping()` peut se verrouiller lui-même

withoutOverlapping() prend un verrou pour qu'une tâche longue ne s'empile pas sur elle-même. Mais si le processus est kill -9 ou que le conteneur meurt en pleine exécution, le verrou n'est pas relâché. Laravel l'expire au bout de *24 heures* par défaut — une tâche peut donc sauter en silence pendant une journée entière. Passez une expiration explicite (->withoutOverlapping(10) = 10 minutes) pour qu'un verrou coincé se répare seul, et laissez le ping /fail vous dire que c'est arrivé.

`->everyFiveMinutes()` a quand même besoin du cron à la minute

Les helpers fréquents (everyMinute, everyFiveMinutes, everyThirtySeconds) ne sont pas magiques — ils sont toujours évalués par le schedule:run d'une fois par minute. everyThirtySeconds() ne marche que parce que schedule:run dort et re-vérifie dans la minute. Si le cron maître ne se déclenche pas, aucun de ces helpers ne tourne, quelle que soit la fréquence annoncée.

`queue:work` est un autre processus — ne pas confondre

Le scheduler répartit les tâches planifiées. Les jobs en file (ShouldQueue, dispatch()) sont gérés par un worker *séparé et permanent (`queue:work` / `queue:listen`, sous Supervisor ou Horizon). Un scheduler mort et un worker de file mort sont deux pannes indépendantes, avec deux symptômes indépendants. Si votre tâche `->daily()` dispatch elle-même un job en file, il vous faut les deux* en vie — surveillez le scheduler avec un heartbeat ici, et le worker de file séparément.

FAQ

Comment savoir si le scheduler Laravel tourne vraiment ?

php artisan schedule:list n'affiche que ce qui est défini, pas ce qui a tourné. Le vrai test : ajouter un Schedule::call() qui pingue une URL de surveillance toutes les quelques minutes. Si le ping cesse d'arriver, le scheduler est mort. Vérifier crontab -l pour la ligne schedule:run et systemctl status cron couvre le côté OS ; le heartbeat prouve que Laravel a bien démarré et bouclé.

Où mettre le planning en Laravel 11 et 12 ?

Dans routes/console.php, avec la façade ScheduleSchedule::command('...')->daily(). L'ancien app/Console/Kernel.php avec sa méthode schedule() marche encore si vous l'avez gardé, et c'est la seule option sur Laravel 10 et avant. Les deux pilotent le même schedule:run.

Quelle différence entre schedule:run et schedule:work ?

schedule:run évalue le planning une fois puis sort — c'est la commande que la crontab appelle chaque minute. schedule:work est une boucle au premier plan qui appelle schedule:run toute seule chaque minute, sans crontab. C'est ce qu'on lance dans un conteneur Docker. Dans les deux cas, si le processus n'est pas vivant, rien n'est réparti.

Les hooks de ping (pingOnSuccess) demandent un package ou de la config ?

Non. pingBefore, thenPing, pingOnSuccess et pingOnFailure sont intégrés au scheduler et utilisent Guzzle, déjà requis par Laravel. Vous passez une URL ; Laravel fait un GET quand le hook se déclenche. Pas de file, pas de package, aucune config de tâche planifiée.

Mon scheduler marche en local mais pas en prod après déploiement — pourquoi ?

Sur Forge, vérifiez que le toggle Scheduler du site a bien re-ajouté le cron ; sur Docker, que le conteneur schedule:work a redémarré et n'est pas tué par l'OOM ; sur tout hébergement, lancez php artisan schedule:run à la main et lisez la sortie — la crontab la cache dans /dev/null. Un check heartbeat avec une période de 5 minutes vous aurait envoyé un mail à l'instant où le déploiement l'a cassé.

Sachez en 5 minutes quand votre scheduler meurt

Ajoutez un heartbeat Schedule::call() et des hooks ->pingOnFailure() par tâche. Si schedule:run s'arrête — crontab effacée, conteneur tué par l'OOM, déploiement cassé — Cron-Ping vous alerte par e-mail ou Slack, au lieu de le découvrir quand le rapport du lundi n'est jamais parti.

Créer un check gratuit

Plan gratuit : 10 checks, alertes email, historique 7 jours. Sans carte. Hébergé en UE.

À lire ensuite

Surveiller les crons Supabase (pg_cron + Edge Functions)Le même problème de scheduler silencieux sur Supabase — un ping depuis pg_net ou l'Edge Function elle-même.Alerte quand une sauvegarde ne tourne plusLe dead man's switch appliqué au job qu'on ne remarque que trop tard.Surveiller un cron Linux qui ne tourne plusLa couche OS : crond, /var/mail, et une crontab écrasée.Documentation Cron-PingPings de succès, start et fail, exemples curl/wget/PHP, délais de grâce.

Sources