Le cold start est le défaut le plus visible de Lambda et le plus mal soigné. Beaucoup d'équipes le découvrent en production, appliquent le premier remède trouvé dans un billet de blog, puis apprennent à vivre avec. Voici la méthode que nous suivons, dans l'ordre où elle rapporte, et ce qu'elle a donné sur l'API d'une marketplace cloud B2B : des cold starts ramenés d'environ 8 s à environ 1,5 s.
Mesurer au p99 avant de toucher à quoi que ce soit
Un cold start ne se voit pas dans la latence moyenne. Sur une API à trafic soutenu, l'immense majorité des invocations touche une instance déjà initialisée ; les démarrages à froid se concentrent dans la queue de la distribution. Optimiser sans regarder le p99, c'est ne savoir ni d'où l'on part, ni si le correctif a servi à quelque chose. C'est pourtant ainsi que la plupart des chantiers commencent : quelqu'un a vu une requête lente, quelqu'un d'autre a lu un article sur la provisioned concurrency, et le lien entre les deux n'a jamais été démontré.
Tout est déjà dans CloudWatch. Chaque cold start écrit un champ initDuration dans la ligne REPORT de l'invocation. Une requête Logs Insights donne la fréquence et la distribution :
filter @type = "REPORT" and ispresent(@initDuration)
| stats count(*) as coldStarts,
avg(@initDuration) as initMoyen,
pct(@initDuration, 99) as initP99
by bin(1h)
La même requête sans le filtre donne le volume total d'invocations ; le rapport entre les deux est le premier chiffre qui compte. Une fonction appelée en continu démarre rarement à froid ; une fonction appelée quelques fois par heure démarre à froid presque à chaque fois. Le même p99 d'initialisation n'a pas la même gravité dans les deux cas, et le travail qui suit n'est pas le même non plus.
C'est aussi le moment de trier. Les chemins synchrones exposés à un utilisateur méritent l'effort ; les workers asynchrones et les jobs planifiés s'accommodent très bien d'une seconde d'initialisation que personne n'attend. Nous avons vu des semaines partir dans l'optimisation de fonctions dont le seul consommateur était une file SQS parfaitement patiente.
La mémoire est un réglage de CPU déguisé
Lambda alloue le CPU proportionnellement à la mémoire : autour de 1 769 Mo, la fonction dispose d'un vCPU entier. Or la phase d'initialisation est presque toujours limitée par le CPU : parser le code, charger les modules, ouvrir les connexions. Une fonction laissée à 128 Mo initialise le même code avec une fraction de vCPU, et cela se paie directement en durée de cold start.
Monter la mémoire est donc souvent le premier gain, et il est contre-intuitif : la fonction coûte plus cher à la milliseconde mais s'exécute moins longtemps, et la facture bouge peu, parfois dans le bon sens. AWS Lambda Power Tuning automatise la recherche du point d'équilibre en exécutant la fonction réelle sur une plage de configurations. Nous le passons sur chaque fonction critique plutôt que d'en débattre en réunion.
Le bundle : là où nous avons le plus gagné
Sur l'API de la marketplace, le levier principal n'était ni la mémoire ni la provisioned concurrency : c'était la taille et la forme du code chargé à l'initialisation. Trois chantiers, menés dans cet ordre :
- Bundling avec esbuild. Passer de
node_modulesdéployés tels quels à un bundle unique, minifié et tree-shaké change la nature du problème : le runtime ne résout plus des milliers de fichiers, il en charge un seul. - Élagage des dépendances. Chaque dépendance doit justifier son poids à l'init. Une bibliothèque entière importée pour formater une date, un client HTTP complet là où
fetchsuffit : ce sont des centaines de millisecondes payées à chaque démarrage, pour rien. - Chargement des clients SDK à la demande. Instancier tous les clients AWS au niveau module se paie au cold start, y compris pour les routes qui n'en utilisent aucun. Nous avons déplacé l'instanciation au premier usage, route par route, en mesurant à chaque fois. C'est un compromis assumé : la première requête concernée paie le prix, et il faut décider où ce prix est acceptable.
La configuration esbuild qui porte l'essentiel du gain tient en une commande :
esbuild src/handler.ts --bundle --minify \
--platform=node --target=node22 \
--external:@aws-sdk/* --outfile=dist/handler.js
C'est cette combinaison, bundling, élagage, chargement paresseux, qui a fait passer les cold starts d'environ 8 s à environ 1,5 s. Aucune ligne de logique métier n'a changé. C'est le point que nous répétons le plus souvent : avant d'acheter de la capacité pour masquer une initialisation lente, il faut vérifier ce que cette initialisation fait réellement.
Vos cold starts dépassent la seconde ? Décrivez votre système : diagnostic d'une page sous 48 h.
Recevoir mon diagnostic →La provisioned concurrency : uniquement là où l'utilisateur la ressent
La provisioned concurrency fonctionne : des instances initialisées attendent le trafic, et le cold start disparaît sur ces instances. Mais elle se facture en continu, trafic ou pas, et elle a un effet pervers : elle rend le problème invisible sans le corriger. Le jour où le trafic dépasse la capacité provisionnée, les invocations excédentaires retrouvent le cold start d'origine, intact, au pire moment possible puisque c'est un pic.
Notre règle : réduire d'abord le cold start réel par le bundle et la mémoire, provisionner ensuite, et uniquement sur les chemins synchrones que des utilisateurs attendent réellement, une authentification, une recherche, un paiement. Jamais sur les workers. Avec Application Auto Scaling, la capacité provisionnée peut suivre un planning : élevée aux heures ouvrées, minimale la nuit. C'est la différence entre un coût maîtrisé et une ligne de facture qui enfle en silence.
Le runtime pèse plus qu'on ne veut l'admettre
Node et Python initialisent vite. Java et .NET portent une machine virtuelle et des frameworks qui se paient cash à l'initialisation ; sur la JVM, SnapStart restaure un instantané de runtime déjà initialisé et change réellement la donne, à condition de traiter les pièges de la restauration, générateurs aléatoires et connexions notamment.
Mais soyons honnêtes sur la portée de ce conseil : on ne change pas de runtime pour gagner du cold start. Les quelque 28 microservices Lambda que nous avons construits et opérés sur cette plateforme étaient en Node et TypeScript, et c'est le bundle, pas le langage, qui a fait la différence. Le choix du runtime est un choix d'équipe et d'écosystème ; le cold start y est un critère parmi d'autres, rarement le premier. Au passage, un mythe à enterrer : placer une fonction dans un VPC n'ajoute plus les secondes d'autrefois, l'attachement des interfaces réseau ayant été mutualisé par AWS depuis longtemps.
Le piège du « warming » artisanal
La tentation classique : une règle EventBridge qui pingue la fonction toutes les cinq minutes pour la garder chaude. Nous l'avons croisée sur presque tous les projets repris, et nous la déconseillons systématiquement.
- Un ping maintient une instance chaude. Dès que deux requêtes arrivent en parallèle, la deuxième provoque un cold start complet. Le hack donne l'illusion d'avoir traité le problème alors qu'il ne couvre que le trafic le plus faible.
- Le handler doit reconnaître les pings et les court-circuiter : de la plomberie qui se duplique dans chaque fonction et finit par fuir dans les métriques.
- Le problème de fond, un bundle obèse ou une initialisation trop lourde, reste entier. Il resurgit à chaque déploiement, à chaque montée en charge, à chaque nouvelle fonction.
Un ping toutes les cinq minutes garde une instance chaude. Votre pic de trafic en réclame vingt.
Si le besoin de garder des instances chaudes est réel, c'est exactement ce que la provisioned concurrency fait proprement, avec des garanties et une facturation explicite. Le warming artisanal est la version fragile du même achat, sans les garanties.
L'ordre qui rapporte
- Mesurer : proportion de cold starts et p99 d'
initDuration, fonction par fonction. - Trier : ne travailler que les chemins synchrones que des utilisateurs attendent.
- Dimensionner la mémoire avec Power Tuning, pas au jugé.
- Réduire le bundle : esbuild, élagage des dépendances, chargement des SDK à la demande.
- Provisionner la concurrency sur les seuls endpoints critiques, après tout le reste.
- Ne pas installer de warming artisanal, et démonter celui qui existe.
- Re-mesurer, et garder la requête Logs Insights dans un tableau de bord permanent.
Le cold start n'est qu'un goulot parmi d'autres. La démarche reste la même face à une requête SQL lente, une réindexation interminable ou une API qui plie sous le trafic : profiler, corriger le plus gros coût, prouver le gain, recommencer. C'est ainsi que nous menons chaque mission d'optimisation de performance.