Aller au contenu
Toova
Tous les outils

5 Expressions Cron Utilisées en Production Réelle

Toova

Les expressions cron semblent simples — cinq champs, une poignée d'opérateurs — mais les équipes de production font les mêmes erreurs à répétition : des tâches qui sautent silencieusement certains mois, des exécutions qui se chevauchent et corrompent l'état partagé, et des centaines de tâches qui se réveillent toutes à la même seconde et surchargent une base de données. L'expression est rarement le problème ; le problème, c'est de ne pas anticiper les cas limites.

Ce guide couvre cinq modèles cron que les équipes d'ingénierie utilisent réellement en production, avec le raisonnement derrière chacun et les pièges à éviter. Utilisez le Analyseur Cron Toova pour vérifier n'importe quelle expression et voir les dix prochaines exécutions avant de déployer.

Anatomie d'une expression cron

Une expression cron standard comporte cinq champs séparés par des espaces :

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- jour de la semaine (0-6, 0=dimanche)
  |   |  |  +------- mois (1-12)
  |   |  +---------- jour du mois (1-31)
  |   +------------- heure (0-23)
  +----------------- minute : toutes les 15e (0, 15, 30, 45)

Chaque champ accepte : une valeur spécifique (5), un joker (*), une plage (1-5), une liste (1,3,5) ou une valeur de pas (*/15). Certaines implémentations ajoutent un sixième champ pour les secondes ; d'autres (comme GitHub Actions) attendent exactement cinq champs. Vérifiez toujours quel dialecte utilise votre planificateur avant d'écrire des expressions complexes.

Modèle 1 : Toutes les 15 minutes — */15 * * * *

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- jour de la semaine (0-6, 0=dimanche)
  |   |  |  +------- mois (1-12)
  |   |  +---------- jour du mois (1-31)
  |   +------------- heure (0-23)
  +----------------- minute : toutes les 15e (0, 15, 30, 45)

L'expression incontournable pour les processeurs de file d'attente, les vérifications de pulsation et les tâches de polling qui doivent s'exécuter fréquemment mais pas en continu. Elle se déclenche à :00, :15, :30 et :45 de chaque heure, chaque jour.

Dans un crontab :

# entrée crontab — exécuter le processeur de file toutes les 15 minutes
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1

En Node.js :

// Node.js avec node-cron
import cron from 'node-cron';

cron.schedule('*/15 * * * *', async () => {
  const batch = await db.query(
    'SELECT * FROM jobs WHERE status = $1 LIMIT 100',
    ['pending']
  );
  await processBatch(batch);
});

Le problème du thundering herd. Lorsque vous exécutez des centaines de workers avec la même expression */15, ils se déclenchent tous simultanément à :00, :15, :30 et :45. Si chaque worker accède à une base de données partagée, vous obtenez quatre pics par heure au lieu d'une charge lissée. La solution consiste à ajouter un jitter aléatoire avant chaque exécution :

// Ajouter du jitter pour éviter le thundering herd sur les systèmes distribués
cron.schedule('*/15 * * * *', async () => {
  const jitterMs = Math.floor(Math.random() * 30_000); // jusqu'à 30s
  await sleep(jitterMs);
  await processBatch();
});

Jusqu'à 30 secondes de jitter répartit le pic sur une fenêtre de 30 secondes tout en maintenant la tâche dans le cycle de 15 minutes prévu. Pour un décalage plus sophistiqué, consultez le Modèle 5 (syntaxe Jenkins H).

Utilisez le Convertisseur de Timestamp pour traduire votre prochaine exécution planifiée depuis l'époque Unix vers une heure lisible lors du débogage des horaires.

Modèle 2 : Heures ouvrées en semaine — 0 9 * * 1-5

0   9   *   *   1-5
|   |   |   |   |
|   |   |   |   +-- jour de la semaine : lundi–vendredi (1-5)
|   |   |   +------ mois : chaque mois
|   |   +---------- jour du mois : chaque jour
|   +-------------- heure : 9
+------------------ minute : 0 (exactement à l'heure pile)

Cette expression se déclenche une fois, exactement à 9h00, du lundi au vendredi. C'est le modèle standard pour : envoyer des e-mails de digest quotidien, exécuter des rapports pendant les heures ouvrées, déclencher des workflows nécessitant une révision humaine pendant la journée de travail, et déployer sur l'environnement de staging au début de la journée d'ingénierie.

Dans un Kubernetes CronJob :

# Kubernetes CronJob — e-mail digest en semaine à 9h UTC
apiVersion: batch/v1
kind: CronJob
metadata:
  name: digest-email
spec:
  schedule: "0 9 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: mailer
            image: myapp/mailer:latest
            command: ["/app/send-digest"]
          restartPolicy: OnFailure

Piège des fuseaux horaires. Les planifications Kubernetes CronJob s'exécutent dans le fuseau horaire du cluster, qui est UTC par défaut. « 9h UTC » correspond à 5h Eastern en hiver et 4h Pacific. Vos utilisateurs recevront leur « digest du matin » en pleine nuit. Utilisez une bibliothèque cron qui accepte les chaînes de fuseau IANA :

// Gestion du fuseau horaire — "9h UTC" est rarement ce qu'attendent les utilisateurs
// Utilisez une bibliothèque qui comprend les fuseaux IANA
import { CronJob } from 'cron';

const job = new CronJob(
  '0 9 * * 1-5',
  () => sendDailyDigest(),
  null,
  true,
  'America/New_York' // s'exécute à 9h Eastern, pas à 9h UTC
);

Le Convertisseur de Fuseau Horaire vous aide à trouver l'équivalent UTC pour une heure locale donnée dans n'importe quel fuseau IANA.

Numérotation des jours de la semaine. La définition cron standard utilise 0 ou 7 pour le dimanche. Certaines implémentations (Quartz, cron4j) utilisent 1 pour le lundi au lieu de 0. Consultez la documentation de votre planificateur et testez avec l'Analyseur Cron avant de déployer.

Modèle 3 : Traitement nocturne — 0 22 * * *

0   22   *   *   *
|    |   |   |   |
|    |   |   |   +-- jour de la semaine : tous
|    |   |   +------ mois : tous
|    |   +---------- jour du mois : tous
|    +-------------- heure : 22 (22h00)
+------------------- minute : 0

S'exécute une fois par jour à 22h00. Le traitement nocturne est l'un des modèles les plus courants en ingénierie des données : agréger les événements de la journée, reconstruire les index de recherche, générer des rapports, archiver les journaux, envoyer des notifications de fin de journée.

Pourquoi 22h00 plutôt que minuit ? Deux raisons. Premièrement, 22h00 offre une marge avant le changement de jour, réduisant le risque de rater des événements arrivant en retard. Deuxièmement, exécuter une tâche lourde à minuit signifie qu'elle entre en concurrence avec la facturation mensuelle (Modèle 4) le premier de chaque mois.

Les trois façons les plus courantes d'écrire une tâche quotidienne :

# Trois façons d'écrire "tous les soirs à 22h" :
0 22 * * *    # cron standard
@daily        # minuit — PAS la même chose que 22h
0 22 * * *    # préférez toujours l'heure explicite aux macros pour les heures non-minuit

Prévention des chevauchements. Une tâche nocturne qui prend normalement 45 minutes peut parfois prendre 2 heures — et si elle est encore en cours lorsque le déclencheur de la nuit suivante se déclenche, les deux tâches modifieront les mêmes données. Utilisez un verrou distribué :

// Vérification d'idempotence — éviter le double traitement si la tâche se chevauche
async function runNightlyBatch() {
  const lock = await redis.set(
    'nightly-batch-lock',
    process.pid,
    'EX', 3600,   // TTL : 1 heure
    'NX'          // ne définir que si n'existe pas
  );

  if (!lock) {
    console.log('Batch déjà en cours, passage.');
    return;
  }

  try {
    await processNightlyData();
  } finally {
    await redis.del('nightly-batch-lock');
  }
}

Le flag Redis NX garantit qu'un seul processus peut détenir le verrou à la fois. Le TTL empêche une tâche plantée de maintenir le verrou indéfiniment.

Modèle 4 : Facturation mensuelle — 0 0 1 * *

0   0   1   *   *
|   |   |   |   |
|   |   |   |   +-- jour de la semaine : tous
|   |   |   +------ mois : tous
|   |   +---------- jour du mois : 1 (premier)
|   +-------------- heure : 0 (minuit)
+------------------ minute : 0

Se déclenche à minuit le premier jour de chaque mois. Le modèle classique pour : générer des factures mensuelles, réinitialiser les quotas d'utilisation, exécuter la réconciliation de fin de mois et archiver les données du mois précédent.

Pièges cron pour les tâches mensuelles :

# Premier du mois à minuit UTC
0 0 1 * *

# ---- PIÈGES À ÉVITER ----

# Ceci ne s'exécute PAS "chaque mois" de façon fiable :
0 0 29-31 * *   # les mois avec moins de jours sont silencieusement ignorés

# Facturation trimestrielle (jan., avr., juil., oct.) :
0 0 1 1,4,7,10 *

# La fin de mois est délicate — il n'existe pas de "dernier jour" en cron standard
# Utilisez @reboot + logique applicative, ou un cron quotidien qui vérifie
# si aujourd'hui est le dernier jour du mois dans le code.

Facturation en fin de mois. Si vous devez facturer le dernier jour de chaque mois (plutôt que le premier), il n'existe pas d'expression cron propre pour cela. La solution de contournement :

// Vérification applicative "dernier jour du mois"
cron.schedule('0 23 28-31 * *', async () => {
  const today = new Date();
  const tomorrow = new Date(today);
  tomorrow.setDate(today.getDate() + 1);

  // Si demain est le 1er, aujourd'hui est le dernier jour du mois
  if (tomorrow.getDate() === 1) {
    await runEndOfMonthBilling();
  }
});

Ceci se déclenche les jours 28 à 31 de chaque mois, puis vérifie dans le code si aujourd'hui est réellement le dernier jour. La tâche s'exécute jusqu'à quatre fois inutilement (pour les mois de moins de 31 jours), mais la vérification est peu coûteuse et la logique est claire.

Pour la facturation trimestrielle, utilisez l'opérateur de liste dans le champ du mois : 0 0 1 1,4,7,10 * (janvier, avril, juillet, octobre).

Modèle 5 : Planification hachée (syntaxe H de Jenkins) — H/30 * * * *

H/30 * * * *
|
+--- H signifie que Jenkins choisit une minute aléatoire stable par tâche.
     H/30 = une fois toutes les 30 minutes, en démarrant à H.
     Différentes tâches obtiennent des valeurs H différentes, répartissant la charge.

Le token H est une extension Jenkins-spécifique à la syntaxe cron. Il remplace H par un nombre pseudo-aléatoire stable dérivé du nom de la tâche. H/30 dans le champ des minutes signifie « une fois toutes les 30 minutes, en démarrant à une minute déterminée par un hachage du nom de la tâche ».

Dans un Jenkinsfile :

// Jenkinsfile — pipeline déclenché toutes les 30 minutes, réparti
pipeline {
  triggers {
    cron('H/30 * * * *')
  }
  stages {
    stage('Deploy') {
      steps {
        sh './deploy.sh'
      }
    }
  }
}

Pourquoi c'est important. Les installations Jenkins ont souvent des centaines de pipelines. Si chaque pipeline utilise */30 * * * *, ils se déclenchent tous à :00 et :30, créant d'énormes pics de charge sur le pool d'agents et les systèmes en aval. Avec H/30, chaque tâche obtient un décalage différent — l'une à :03/:33, une autre à :17/:47 — répartissant la charge uniformément sur l'heure.

GitHub Actions ne prend pas en charge H. Pour décaler plusieurs workflows Actions, choisissez manuellement des décalages de minutes fixes différents :

// GitHub Actions — H n'est pas supporté ; décalez manuellement
// Pour répartir deux tâches : choisissez des minutes fixes différentes
name: Job A
on:
  schedule:
    - cron: '5 * * * *'   # s'exécute à :05 de chaque heure

---
name: Job B
on:
  schedule:
    - cron: '35 * * * *'  # s'exécute à :35 — 30 minutes après Job A

C'est moins élégant que le H de Jenkins mais produit le même effet pour un petit nombre de workflows. Pour un grand nombre de workflows, envisagez un orchestrateur externe qui prend en charge nativement la planification distribuée.

Considérations transversales

L'idempotence est non négociable

Chaque tâche cron doit être sûre à exécuter deux fois. L'infrastructure tombe en panne, les planificateurs redémarrent et les verrous distribués finissent parfois par expirer. Concevez des tâches de sorte qu'une deuxième exécution — avec les mêmes entrées, au même moment — produise le même résultat. Utilisez des upserts plutôt que des inserts, des patterns de vérification avant action, et des clés d'idempotence pour les appels API externes.

Alertes sur file morte

Une tâche cron qui échoue silencieusement est pire que celle qui ne s'exécute pas du tout. Envoyez une alerte vers une file morte ou un système de monitoring lorsqu'une tâche échoue ou ne se termine pas dans la fenêtre de temps prévue. Des outils comme Sentry Crons, Healthchecks.io et Cronitor ajoutent une vérification de pulsation : la tâche envoie un ping à une URL au démarrage et à la fin. Si le ping de fin n'arrive pas dans le délai imparti, une alerte se déclenche.

Journaliser la planification

Émettez une ligne de journal structurée au début et à la fin de chaque exécution cron, incluant l'expression, l'horodatage de déclenchement (pas l'heure actuelle, qui peut différer légèrement à cause du jitter) et la durée. Cela facilite la corrélation d'un incident en aval — « la base de données a ralenti à 22h » — avec l'exécution d'une tâche batch spécifique.

Référence rapide

Analysez et validez n'importe quelle expression avec l'Analyseur Cron. Convertissez les heures planifiées entre fuseaux horaires avec le Convertisseur de Fuseau Horaire. Si vos journaux de tâche contiennent des timestamps Unix, le Convertisseur de Timestamp les transforme instantanément en dates lisibles.

Pour une référence complète de la syntaxe cron et des exemples courants, crontab.guru est la référence rapide canonique. Pour la syntaxe H de Jenkins et sa spécification complète, consultez la documentation de la syntaxe cron Jenkins Pipeline.