Node.js Internals

L'Event Loop de Node.js

Node exécute votre JavaScript sur un seul thread, tout en gérant des milliers de connexions. L'event loop et ses phases sont la raison de ce succès — et pourquoi le bloquer est problématique.

intermediate15 min readUpdated 15 sept. 2026
order.js
js
// order.js
console.log("1: synchronous");

setTimeout(() => console.log("5: timers phase"), 0);
setImmediate(() => console.log("6: check phase"));

Promise.resolve().then(() => console.log("3: microtask"));

process.nextTick(() => console.log("2: nextTick"));
Threads
Un seul thread principal
Moteur
V8 plus libuv
Phases
Timers, poll, check, close
Microtasks
nextTick et promises
Thread pool
Taille par défaut : 4
Blocage
Paralyse tout le système

Pourquoi c'est important

Pourquoi l'event loop est crucial

Un seul thread, plusieurs connexions

La boucle multiplexe des milliers de sockets sur un seul thread en ne attendant jamais de manière synchrone.

Un ordre prévisible

La connaissance des phases et des files d'attente permet de raisonner sur le moment où les callbacks s'exécutent.

Le blocage est l'ennemi

Toute tâche synchrone longue gèle toutes les autres requêtes, c'est pourquoi les travaux lourds sont déportés hors de la boucle.

Le tableau complet

Les trois piliers de la boucle

Les phases décident de l'ordre d'exécution, les microtasks s'exécutent entre les phases, et le thread pool gère les tâches qui ne peuvent pas être non-bloquantes.

Phases

Planification

La boucle cycle à travers les timers, les callbacks en attente, le poll, le check et les callbacks de fermeture.

Queues

Priorisation

Les microtasks sont vidées entre les phases, et nextTick s'exécute avant les callbacks de promises.

Thread pool

Déchargement

Le travail lié aux fichiers, au DNS et à la crypto s'exécute sur le thread pool de libuv pour que la boucle reste libre.

L'event loop en un coup d'œil

Les concepts fondamentaux

Phase Timers

Exécute les callbacks planifiés par setTimeout et setInterval qui sont arrivés à échéance.

Phase Poll

Récupère les nouveaux événements I/O et exécute leurs callbacks, se mettant en attente si elle est inactive.

Phase Check

Exécute les callbacks setImmediate juste après la phase poll.

Microtasks

process.nextTick et les callbacks de promises s'exécutent entre les phases.

Thread pool

libuv exécute fs, DNS et certaines opérations crypto sur des worker threads.

Monitoring

Surveillez le délai de la boucle et l'utilisation de l'event loop pour détecter les blocages.

Un bref aperçu

Des serveurs bloquants à l'I/O piloté par les événements

  1. 2009

    Node piloté par les événements

    Node associe V8 à libuv pour exécuter JavaScript avec des I/O non-bloquantes.

    09
  2. 2011

    Extraction de libuv

    L'event loop et les abstractions de plateforme deviennent une bibliothèque autonome.

    11
  3. 2015

    Promises et microtasks

    Les promises natives ajoutent une nouvelle file d'attente avec des règles d'ordonnancement claires.

    15
  4. 2020

    Meilleurs diagnostics

    Des outils comme l'utilisation de l'event loop rendent le blocage mesurable.

    20
  5. Today

    Le même modèle partout

    L'event loop soutient les serveurs, les outils, les runtimes serverless et même les navigateurs.

    Today

Le guide complet

L'Event Loop de Node.js: Tout ce que vous devez savoir

Pourquoi l’event loop est essentielle

Node.js exécute votre JavaScript sur un seul thread principal, et pourtant, un seul serveur peut gérer des milliers de connexions simultanées. C’est possible parce que l’event loop n’attend jamais. Lorsqu’une opération risquerait de bloquer, Node la délègue au système d’exploitation ou à un pool de threads et continue d’effectuer d’autres tâches. Une fois le résultat prêt, un callback est mis en file d’attente et la boucle l’exécute.

Comprendre l’event loop permet d’expliquer nombre de comportements de Node : pourquoi une lecture de fichier synchrone fige l’intégralité du serveur, pourquoi une promesse est logguée avant un timer avec un délai nul, et pourquoi les tâches lourdes pour le CPU nécessitent un worker. C’est le modèle mental le plus important pour écrire du code Node performant.

La boucle et ses phases

L’event loop est un cycle de phases. Chaque phase possède une file d’attente de callbacks, et la boucle les traite avant de passer à la suivante.

  1. Timers — callbacks planifiés par setTimeout et setInterval arrivés à échéance.
  2. Pending callbacks — callbacks système différés, comme certaines erreurs TCP.
  3. Idle, prepare — maintenance interne.
  4. Poll — récupère les nouveaux événements I/O et exécute leurs callbacks. Si rien n’est prêt, elle peut bloquer brièvement à cette étape.
  5. Check — exécute les callbacks setImmediate.
  6. Close callbacks — gère les événements tels que la fermeture d’un socket.

La boucle continue de tourner tant qu’il y a du travail à effectuer. Lorsqu’il n’y a plus de timers, plus d’I/O en attente et plus de handles, le processus s’arrête.

Microtasks : nextTick et promises

Entre les phases, et après chaque callback, Node vide deux files d’attente prioritaires :

  • Les callbacks process.nextTick, qui s’exécutent en premier.
  • Les callbacks de Promise, qui s’exécutent après nextTick.

Toutes deux sont entièrement vidées avant que la boucle n’avance, ce qui a deux conséquences. Premièrement, elles s’exécutent avant les timers et les callbacks d’I/O planifiés précédemment. Deuxièmement, un nextTick récursif ou une chaîne de promises infinie peut affamer la boucle (starve the loop), empêchant ainsi l’exécution de l’I/O.

// order.js
console.log("1: synchronous");

setTimeout(() => console.log("5: timers"), 0);
setImmediate(() => console.log("6: check"));

Promise.resolve().then(() => console.log("3: promise"));
process.nextTick(() => console.log("2: nextTick"));

Le résultat est 1, 2, 3, puis 5 et 6. L’ordre de 5 et 6 peut varier au niveau racine (top level) car il dépend du temps nécessaire pour atteindre la phase des timers ; à l’intérieur d’un callback d’I/O, setImmediate s’exécute toujours avant setTimeout.

Le pool de threads de libuv

Certaines opérations ne peuvent pas être effectuées de manière asynchrone par l’OS de façon portable : l’accès au système de fichiers, les résolutions DNS et certaines fonctions cryptographiques. libuv exécute ces tâches sur un pool de threads, qui compte quatre threads par défaut, configurable via UV_THREADPOOL_SIZE.

C’est pourquoi fs.readFile est non-bloquant pour votre code, même s’il n’est pas piloté par des événements en interne. Cela signifie également qu’une rafale d’opérations sur les fichiers peut s’accumuler dans une file d’attente derrière un pool de quatre threads. Les API async libèrent le thread principal ; le pool, lui, gère l’attente.

Bloquer l’event loop

Tout ce qui est synchrone et lent sur le thread principal bloque absolument tout : les autres requêtes, les timers et les E/S. Les causes les plus fréquentes sont :

  • readFileSync et autres appels de fichiers synchrones dans les gestionnaires de requêtes.
  • De gros JSON.parse ou JSON.stringify sur des payloads volumineux.
  • La cryptographie synchrone, comme pbkdf2Sync avec un nombre élevé d’itérations.
  • Les boucles serrées, le backtracking de regex et les transformations de données lourdes.
  • Les bibliothèques bloquantes qui effectuent un travail CPU sur le thread principal.

Mesurez plutôt que de deviner : le délai de l’event loop (event loop delay) et l’utilisation de l’event loop (event loop utilization) vous indiquent combien de temps la boucle reste bloquée. Une augmentation du délai sous charge est le signal d’alerte.

Décharger l’event loop

Lorsque le travail est réellement limité par le CPU (CPU-bound), déplacez-le :

  • worker_threads pour les calculs au sein du même processus, via le passage de messages.
  • child processes pour les tâches plus lourdes ou isolées.
  • Un service séparé pour les charges de travail les plus massives, avec un scaling indépendant.
  • Le chunking : divisez les tâches volumineuses en petits morceaux et laissez le contrôle entre chaque segment.

Pour les entrées/sorties (I/O), utilisez les API async et les streams afin que la loop reste libre.

Bonnes pratiques

  • Utilisez des API async dans les gestionnaires de requêtes ; ne bloquez jamais la boucle.
  • Laissez la priorité aux E/S avec setImmediate plutôt qu’avec des nextTick récursifs.
  • Déplacez les tâches gourmandes en CPU vers des worker threads ou des processus enfants.
  • Utilisez des flux (streams) pour les données volumineuses au lieu de tout mettre en mémoire tampon.
  • Ajustez UV_THREADPOOL_SIZE uniquement après avoir effectué des mesures.
  • Surveillez le délai et l’utilisation de la boucle d’événements en production.
  • Gardez des chaînes de callbacks courtes pour éviter que les microtâches ne privent les E/S de ressources.

Erreurs courantes

  • Utiliser readFileSync dans un “hot path” (chemin critique).
  • Des process.nextTick récursifs qui saturent la boucle.
  • Supposer que Node.js est entièrement mono-thread et ignorer le pool de threads.
  • Effectuer des calculs lourds en ligne au lieu de les déléguer.
  • S’attendre à ce que setTimeout(fn, 0) s’exécute avant les callbacks d’I/O.
  • Ignorer le délai de l’event loop jusqu’à ce que des problèmes de latence apparaissent.

Et après ?

L’event loop est le cœur de Node.js. Poursuivez avec les Streams pour les E/S par morceaux (chunked I/O) et le File System pour l’accès asynchrone aux fichiers. Revenez aux Bases de Node.js si le runtime est encore nouveau pour vous, puis mesurez le délai de l’event loop sur l’un de vos services.

Lecture d'un fichier

Le travail asynchrone s'exécute sur le thread pool et libère la boucle. Un appel synchrone bloque toutes les autres requêtes pendant que le disque répond.

Préférer
import { readFile } from "node:fs/promises";

const data = await readFile("big.log", "utf8");
res.end(data);
Éviter
import { readFileSync } from "node:fs";

// blocks the entire server
const data = readFileSync("big.log", "utf8");
res.end(data);

Planification du travail

nextTick s'exécute avant les callbacks de promises et avant que la boucle ne continue. setImmediate s'exécute dans la phase check après l'I/O.

Préférer
// after current I/O, don't starve
setImmediate(() => processBatch());
Éviter
// recursive nextTick starves
// the loop and blocks I/O
process.nextTick(() => loop());

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre Event Loop ?

Notre tutoriel interactif vous guide à travers Event Loop pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.