Pour exécuter une fonction AWS Lambda à heure fixe, la planification se crée en dehors de la fonction et pointe vers son ARN. En 2026, cela veut dire une planification EventBridge Scheduler : une expression cron(), rate() ou at(), un fuseau horaire, une cible et sa propre politique de retry. Les anciennes règles cron EventBridge fonctionnent toujours, se déclenchent toujours en UTC uniquement, et ne méritent d'être gardées que pour les jobs qui tournent déjà dessus. Le câblage tient en dix lignes de YAML ; ce qui casse ensuite, ce sont les fuseaux, les doubles déclenchements et le silence.
Comment planifier une fonction Lambda ?
Lambda n'a pas d'horloge interne. Une fonction ne se réveille jamais toute seule : quelque chose l'invoque de l'extérieur à heure dite, et la seule vraie décision porte sur ce quelque chose. Trois options existent en pratique, et seules les deux premières planifient réellement.
EventBridge Scheduler est un service conçu pour ça : cron avec fuseau horaire, exécutions ponctuelles, politique de retry et file de lettres mortes par planification, quotas qui se comptent en millions. Les anciennes règles planifiées EventBridge sont le mécanisme historique, rattaché à un bus d'événements, en UTC uniquement. Les états Wait de Step Functions sont la troisième voie, et ils ne planifient rien : ils suspendent un workflow déjà en cours. Nous comparons les trois dans notre article EventBridge Scheduler contre règles cron ; celui-ci parle de la construction du job.
Quel que soit le choix, les pièces sont les mêmes : une expression de planification, une cible (l'ARN de la fonction), un rôle IAM qui autorise l'invocation, et une charge utile JSON facultative que le handler reçoit comme événement.
Comment créer une planification EventBridge Scheduler ?
Quand le job est livré avec la fonction, déclarez-le dans SAM ou CloudFormation. Le type d'événement SAM s'appelle ScheduleV2 : il crée une planification EventBridge Scheduler et le rôle d'invocation. Attention au nom : le type Schedule, plus ancien, crée une règle historique à la place, et deux caractères séparent les deux.
# template.yaml : la planification est livrée avec la fonction
Resources:
NightlyReport:
Type: AWS::Serverless::Function
Properties:
Handler: report.handler
Runtime: nodejs22.x
Events:
Semaine:
Type: ScheduleV2
Properties:
ScheduleExpression: cron(30 6 ? * MON-FRI *)
ScheduleExpressionTimezone: Europe/Paris
FlexibleTimeWindow:
Mode: "OFF"
RetryPolicy:
MaximumRetryAttempts: 3
DeadLetterConfig:
Arn: !GetAtt ScheduleDlq.Arn
Pensez à mettre ce OFF entre guillemets : sans eux, YAML le lit comme un booléen et le déploiement échoue sur une erreur de type. Pour les planifications créées à l'exécution, une par utilisateur ou par commande, les mêmes champs existent côté API. Notez leur emplacement : la politique de retry et la file de lettres mortes appartiennent à la cible, pas à la planification.
// Créée à l'exécution avec le SDK AWS v3
import { SchedulerClient, CreateScheduleCommand } from "@aws-sdk/client-scheduler";
const scheduler = new SchedulerClient({});
await scheduler.send(new CreateScheduleCommand({
Name: "nightly-report",
ScheduleExpression: "cron(30 6 ? * MON-FRI *)",
ScheduleExpressionTimezone: "Europe/Paris",
FlexibleTimeWindow: { Mode: "OFF" },
Target: {
Arn: "arn:aws:lambda:eu-west-1:123456789012:function:nightly-report",
RoleArn: "arn:aws:iam::123456789012:role/scheduler-invoke-lambda",
Input: JSON.stringify({ job: "nightly-report" }),
RetryPolicy: { MaximumRetryAttempts: 3, MaximumEventAgeInSeconds: 3600 },
DeadLetterConfig: { Arn: "arn:aws:sqs:eu-west-1:123456789012:schedule-dlq" },
},
}));
Deux champs méritent l'attention. FlexibleTimeWindow sur OFF signifie « à cette minute précise » ; sur FLEXIBLE avec une fenêtre, Scheduler étale les invocations dessus, ce qui est exactement ce qu'il faut quand des centaines de planifications tombent sur la même minute. Et une expression at() donne une exécution ponctuelle, avec ActionAfterCompletion sur DELETE pour que la planification se supprime après déclenchement.
Comment fonctionnent les règles cron EventBridge, et quand les garder ?
Une règle planifiée accepte deux formes d'expression. rate(valeur unité) est la plus simple : rate(5 minutes), rate(1 hour), rate(7 days). L'unité est au singulier pour la valeur 1 et au pluriel au-delà, et la minute est le plancher.
C'est cron() qui réserve les surprises. AWS utilise six champs, pas les cinq de la crontab Unix : minute, heure, jour du mois, mois, jour de la semaine et année. Le jour de la semaine commence au dimanche, qui vaut 1, donc du lundi au vendredi s'écrit 2-6 ou MON-FRI. Et vous ne pouvez pas donner une valeur à la fois au jour du mois et au jour de la semaine : l'un des deux doit être un point d'interrogation. Un job quotidien à 8 h s'écrit cron(0 8 * * ? *). La version à cinq champs recopiée d'une crontab Linux est rejetée, et cette même syntaxe à six champs s'applique aussi à Scheduler.
# Règle historique : même fonction, UTC, aucun champ de fuseau n'existe
Events:
Nightly:
Type: Schedule
Properties:
Schedule: cron(0 5 ? * MON-FRI *)
Tout ce qu'une règle déclenche part en UTC, et une règle ne sait pas exprimer une exécution ponctuelle. Reste un seul cas raisonnable pour les conserver : un petit ensemble stable de jobs réellement UTC, déjà déployés, déjà supervisés, et qu'une migration n'améliorerait en rien.
Scheduler ou règle cron : que choisir pour votre job ?
Pour un nouveau job, la réponse est Scheduler, et le tableau dit pourquoi. Les trois premières lignes sont des capacités qu'une règle n'a tout simplement pas ; la dernière ligne est le seul scénario où les règles gagnent encore.
| Critère | EventBridge Scheduler | Règle cron EventBridge |
|---|---|---|
| Expressions | cron(), rate(), et at() pour le ponctuel | cron() et rate() seulement |
| Fuseau horaire | Tout fuseau IANA, heure d'été gérée | UTC uniquement |
| Retry et DLQ | Par planification : tentatives, âge maximal, file | Par cible sur la règle |
| Cibles | Lambda, plus des centaines d'API AWS en direct | Cibles du bus (Lambda, SQS, SNS, etc.) |
| Échelle | Quotas en millions de planifications | Quelques centaines de règles par bus (quota par défaut) |
| Déclaration | ScheduleV2 en SAM, AWS::Scheduler::Schedule | Schedule en SAM, AWS::Events::Rule |
| Quand il gagne | Nouveaux jobs, heure locale, ponctuel, par utilisateur | Jobs UTC déjà déployés et déjà supervisés |
Des jobs planifiés qui partent à la mauvaise heure, deux fois, ou jamais ? Décrivez votre système : diagnostic d'une page sous 48 h.
Recevoir mon diagnostic →Comment gérer les fuseaux horaires et le changement d'heure ?
Si un humain lit le résultat, la planification appartient à l'heure locale. Renseignez ScheduleExpressionTimezone avec un nom IANA comme Europe/Paris et la planification suit l'horloge locale : 6 h 30 reste 6 h 30 en janvier comme en juillet. Confiez le même job à une règle UTC et il tombe une heure à côté la moitié de l'année, l'incident semestriel classique que personne ne consigne puisque rien n'a échoué.
Deux cas limites méritent d'être connus avant de faire aveuglément confiance à l'heure locale. Quand les horloges avancent, une heure locale située dans l'heure sautée n'existe pas ce jour-là ; quand elles reculent, une heure locale située dans l'heure répétée se présente deux fois au cadran. AWS documente le comportement des deux cas dans la référence des types de planification. La parade simple : garder les planifications hors du créneau nocturne où le basculement se produit, et assumer l'UTC pour les jobs qui ne s'adressent qu'à des machines.
Dans le handler, restez en UTC. Calculez le jour métier à partir de l'heure planifiée transmise par le scheduler, jamais depuis une horloge locale, et journalisez le fuseau supposé par le job pour que le prochain lecteur n'ait pas à le deviner.
Qu'est-ce qui casse une fois en production ?
Une planification peut se déclencher deux fois. La livraison est au moins une fois, un retry peut suivre un timeout pendant que la première invocation tourne encore, et une stack redéployée peut laisser deux planifications pointées sur la même fonction. Rendez donc le job idempotent. Scheduler injecte des attributs de contexte dans la charge utile, ce qui fournit une clé de déduplication sans effort :
// Scheduler remplace ces valeurs au moment de l'invocation
Input: JSON.stringify({
job: "nightly-report",
scheduledTime: "<aws.scheduler.scheduled-time>",
executionId: "<aws.scheduler.execution-id>",
attempt: "<aws.scheduler.attempt-number>",
}),
Écrivez cette clé dans DynamoDB par un put conditionnel avant de commencer le travail, et sortez immédiatement si l'écriture échoue. Les jobs plus longs que leur propre intervalle ont besoin de la même garde pour une autre raison : l'invocation suivante arrive avant la fin de la précédente.
Vient ensuite le silence. Les invocations planifiées sont asynchrones, donc personne n'attend la réponse : Lambda réessaie une invocation asynchrone en échec puis abandonne l'événement, sauf si une destination ou une file de lettres mortes le récupère. Attachez une file de lettres mortes à la planification, alarmez sur sa profondeur, et alarmez sur la métrique Errors de la fonction. Ajoutez l'alarme que la plupart des équipes oublient : invocations inférieures à 1 sur la période, avec les données manquantes traitées comme un dépassement. Un job qui a cessé de se déclencher ne produit aucune erreur : l'alarme d'erreurs reste au vert pendant que le travail ne se fait plus.
Pourquoi le cron de polling est-il un mauvais réflexe ?
L'anti-pattern, c'est une fonction sur rate(1 minute) qui balaie une table à la recherche de travail dû. Elle se déclenche plus de mille fois par jour pour ne rien faire la plupart du temps, ajoute jusqu'à une minute de latence à chaque élément, et se dégrade en silence à mesure que la table grossit. Le remplacement tient en une planification ponctuelle par élément, créée en même temps que l'élément, avec ActionAfterCompletion sur DELETE. Avec des quotas en millions, une planification par rappel est l'usage prévu, pas un détournement.
Ne gardez le polling que là où le travail consiste vraiment à « tout regarder à intervalle régulier » : une réconciliation nocturne, un export quotidien, un balayage de nettoyage. L'autre cron de polling à questionner est le ping de maintien au chaud, qui garde exactement un environnement d'exécution tiède et rien de plus, comme nous l'expliquons dans notre article sur les warm starts Lambda.
Le coût suit la même forme. Scheduler facture à l'invocation au-delà d'une franchise mensuelle, les règles planifiées relèvent de la tarification EventBridge, et la fonction elle-même se facture en requêtes et en Go-secondes selon la tarification Lambda. La planification n'est presque jamais la partie chère : c'est la fréquence multipliée par la durée de la fonction. Un polling à la minute paie ce produit toute la journée, tous les jours, quand une planification événementielle ne le paie qu'au moment où quelque chose arrive. Vérifiez les chiffres et les franchises du moment sur les pages officielles avant tout chiffrage.
Si vous mettez ce choix en regard des autres briques AWS, la version courte de chaque arbitrage se trouve dans notre bibliothèque de décisions AWS.
Check-list d'une Lambda planifiée
- Nouveau job : EventBridge Scheduler, déclaré en ScheduleV2 dans SAM, jamais une nouvelle règle historique.
- Sortie lue par un humain à heure fixe : renseignez ScheduleExpressionTimezone, ne codez pas le décalage à la main.
- Six champs cron, un point d'interrogation dans le jour du mois ou le jour de la semaine, dimanche en jour 1.
- Une clé d'idempotence par exécution, écrite conditionnellement avant le travail.
- Une file de lettres mortes sur chaque planification, avec une alarme sur sa profondeur.
- Une alarme sur l'absence d'invocation, données manquantes traitées comme un dépassement.
- Travail par élément : une planification at() auto-supprimée pour chacun, pas un polling à la minute.
- Cible SQS, workflow ou autre API AWS : appelez-la depuis la planification, supprimez la fonction de liaison.