Comment 59 secondes de retard m'ont fait passer de cron à systemd

Les 59 secondes qui m'ont agacé

Mon bot LinkedIn répondait aux commentaires avec un cron qui tournait toutes les minutes. Un commentaire arrive à 10h00, le cron s'exécute à 10h01, le bot répond à 10h01. Ça marche. Mais si le commentaire arrive à 10h01 et une seconde... il attend jusqu'à 10h02. Un jour, un commentaire important d'un recruteur est resté sans réponse pendant 59 secondes.

Pas dramatique. Mais agaçant. Suffisamment agaçant pour que je me demande : est-ce que le cron est vraiment le bon outil ici ?

Spoiler : pour ce cas précis, non. Mais pour d'autres tâches sur la même machine, il reste imbattable. Voici l'histoire de comment j'ai fini avec les deux approches côté à côté, et pourquoi c'est exactement ce qu'il faut.

Chapitre 1 : quand le cron suffisait largement

Tout a commencé de façon très simple. Deux scripts Node.js à lancer à heure fixe : publier un article sur dev.to à 9h, poster sur LinkedIn à 10h. Classique. La crontab tenait en deux lignes :

0 9 * * * bash ~/work/scripts/devto-cron.sh >> logs/devto-cron.log 2>&1
0 10 * * * /usr/bin/node ~/work/scripts/linkedin-cron.js >> logs/linkedin-cron.log 2>&1

Le script shell charge les variables d'environnement depuis un fichier .env et appelle le script Node.js. Ce dernier lit devto-schedule.json, prend le premier article en statut drafted, appelle l'API dev.to, met à jour le fichier, et s'arrête. Durée d'exécution : quelques secondes. En cas d'échec, le log contient l'erreur, l'article reste en queue pour le lendemain.

Et ça tournait. Des mois. Sans jamais avoir besoin d'être relancé manuellement. Le cron est génial pour ça : une tâche atomique, sans état, qui n'a pas besoin de savoir ce qui s'est passé avant elle. Un process démarre, fait son travail, s'arrête. Exactement ce pour quoi le cron existe depuis 1975.

Chapitre 2 : le moment où ça a dérappé

Le problème est arrivé avec le système de veille automatisée. Quatre veilles actives (crypto, tech, Epstein, retro), chacune avec sa fréquence propre (de 6h à 7 jours). L'interface d'administration permettait de déclencher une veille manuellement depuis le navigateur. Un job "generate" pouvait être enfilé à tout moment pour reconfigurer une veille via Claude.

J'ai d'abord essayé le cron. Naturellement. Et j'ai commencé à empiler les problèmes :

  • Granularité minimale de 1 minute. L'utilisateur clique "Relancer" dans l'interface, attend, ne voit rien se passer pendant potentiellement 59 secondes. L'UX était pénible.
  • Pas d'état entre les runs. Pour savoir quand chaque veille avait tourné pour la dernière fois, il fallait lire un fichier JSON à chaque démarrage. Avec quatre veilles qui partagent les mêmes ressources, la coordination devenait fragile.
  • Pas de file de jobs. Deux crons qui se déclenchent simultanément et tentent d'écrire les mêmes fichiers ? Il faut gérer l'overlap avec flock ou équivalent. Ça fonctionne, mais ça demande de l'attention.
  • Pas de log structuré natif. La sortie standard redirigée vers un fichier devenait vite illisible quand plusieurs veilles écrivaient en parallèle.

Aucun de ces problèmes n'est insurmontable individuellement. Mais empilés, ils m'ont fait comprendre que j'essayais de faire faire au cron quelque chose qui n'était plus son terrain.

Chapitre 3 : le daemon entre en scène

La solution : un daemon Node.js qui tourne en continu. veille-daemon.js poll jobs.json toutes les 30 secondes pour les jobs on-demand, et cron-config.json toutes les 60 secondes pour déclencher les veilles planifiées dont la fréquence est dépassée. Il tient en mémoire le registre des veilles et l'état de ce qui tourne.

Quand l'utilisateur clique "Relancer" dans l'interface, le PHP écrit un job dans jobs.json. Le daemon le détecte au prochain poll (au pire 30 secondes), l'exécute, et met à jour le statut. L'interface peut afficher la progression en temps réel via un endpoint /veille/status. L'UX est passée de "je clique et rien ne se passe" à "je clique et ça démarre sous 30 secondes". Nuit et jour.

systemd supervise le daemon avec un fichier .service minimal :

[Unit]
Description=Veille daemon
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/folken/work/cv
ExecStart=/usr/bin/node scripts/veille-daemon.js
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target

Ce que ça apporte concrètement : redémarrage automatique en cas de crash, logs dans journald (journalctl --user -u veille-daemon -f), démarrage automatique au boot avec systemctl --user enable veille-daemon. Plus de script de relance manuel, plus de fichier de log à surveiller séparément.

Mais ça a demandé du travail. Quelques jours pour rendre le daemon robuste : gestion des crashes, TTL sur les états bloqués, récupération après redémarrage. Un cron, c'est trois lignes. Un daemon fiable, c'est un vrai morceau de code.

L'entre-deux que j'ai failli oublier : le systemd timer

Entre le cron minimaliste et le daemon complet, il y a une troisième voie que j'ai sous-estimée pendant longtemps : le systemd timer. C'est un cron amélioré, une tâche périodique, mais supervisée par systemd. Sur ce projet, crypto-veille.timer en est un exemple legacy :

# crypto-veille.service
[Unit]
Description=Crypto veille job
After=network.target

[Service]
Type=oneshot
WorkingDirectory=/home/folken/work/cv
ExecStart=/usr/bin/node scripts/crypto-veille.js
# crypto-veille.timer
[Unit]
Description=Crypto veille, toutes les 6h

[Timer]
OnBootSec=5min
OnUnitActiveSec=6h

[Install]
WantedBy=timers.target

Par rapport à un cron, le timer ajoute : des logs natifs dans journald, la possibilité de déclarer des dépendances (After=network.target), un délai au boot avec OnBootSec (le cron peut se déclencher trop tôt si la machine vient de redémarrer), et des expressions de calendrier plus lisibles que la syntaxe cron classique.

La contrepartie : deux fichiers à gérer au lieu d'une ligne. Pour une tâche simple qui n'a pas besoin de logs structurés et dont la fréquence minimale est 1 minute, le cron reste plus direct. Le timer systemd, c'est le compromis du milieu, quand le cron ne suffit plus mais qu'un daemon serait du sur-engineering.

La carte de synthèse

Après avoir vécu les trois approches sur le même projet, voici comment elles se comparent :

cron systemd timer daemon
Granularité 1 min minimum 1 seconde libre (polling)
Logs fichier manuel journald natif journald natif
État entre runs aucun aucun en mémoire
Réaction on-demand non non oui
Supervision crash non oui oui
Complexité config très faible moyenne (2 fichiers) élevée (code à écrire)
Cas idéal tâche atomique périodique tâche périodique + logs/deps file de jobs, état, on-demand

La règle des trois questions

Après cette expérience, j'ai dégagé trois questions qui suffisent à trancher :

1. La tâche doit-elle réagir à un événement en moins d'une minute ?
Si oui : daemon. Le cron ne peut pas faire mieux qu'une minute, et le timer systemd non plus (il exécute des tâches planifiées, pas réactives).

2. A-t-elle besoin d'état entre les runs ?
Si oui : daemon. Lire et écrire un fichier à chaque run fonctionne jusqu'à un certain point, mais dès qu'il y a plusieurs workers ou des décisions à prendre sur l'historique récent, un process continu avec état en mémoire est plus propre.

3. Est-ce que la supervision et les logs structurés comptent ?
Si oui mais que les deux premières réponses sont non : systemd timer. Il donne les bénéfices de systemd (journald, redémarrage, dépendances) sans la complexité d'écrire un event loop.

Si les trois réponses sont non : cron. Trois lignes dans la crontab, une redirection vers un fichier log, zéro infrastructure supplémentaire. Ne pas sur-engineer ce qui n'en a pas besoin.

Épilogue : les deux coexistent, et c'est bien comme ça

Aujourd'hui, sur la même machine, le cron continue de publier mes articles à heure fixe. Il n'a jamais eu besoin d'être relancé manuellement. Le daemon de veille, lui, a nécessité quelques jours de travail pour être robuste, mais il répond en 30 secondes là où le cron aurait mis jusqu'à une minute.

Ce n'est pas cron OU daemon. C'est reconnaître lequel des deux correspond au problème posé. La publication à heure fixe n'a pas besoin de réagir en 30 secondes. La veille interactive ne peut pas se permettre d'attendre une minute entre deux checks.

Et les 59 secondes de retard sur LinkedIn ? Le bot tourne maintenant en daemon. Le recruteur n'a plus à attendre.

📄 CLAUDE.md associé

Commentaires (0)