Node.js : comment les lookups DNS peuvent bloquer vos performances

En travaillant sur la prochaine version d’une application Node.js pour un client, nous avons découvert que les lookups DNS pouvaient devenir un bottleneck caché, ralentissant un layer critique de l’infrastructure. Heureusement, le correctif est simple.

Quand on parle de performances Node.js, on pense immédiatement à l’event loop, au code non bloquant, au caching applicatif ou encore à l’optimisation des requêtes SQL.
Pourtant, lors du travail sur une application Node.js à fort trafic pour un client, nous avons mis le doigt sur un bottleneck beaucoup plus discret, mais capable de ralentir un layer critique de l’infrastructure : les lookups DNS.

Un problème rarement monitoré, souvent mal compris, et pourtant très réel en production.

Comment Node.js résout les DNS

À chaque requête HTTP sortante (API tierce, micro-service interne, outil de monitoring, service SaaS...), Node.js doit résoudre un nom de domaine en adresse IP. Pour cela, il s’appuie sur dns.lookup(), qui utilise le resolver du système d’exploitation, sans mettre en place de cache DNS côté application.
Résultat : chaque requête HTTP peut déclencher un nouveau lookup DNS, même si le domaine est identique et appelé plusieurs fois par seconde.

Pourquoi c’est un vrai problème de performance

1. Les lookups DNS utilisent le thread-pool libuv

Contrairement à ce que l’on pourrait penser, la résolution DNS n’est pas gérée par l’event loop principal.
Elle passe par le thread-pool libuv, qui contient 4 threads par défaut.
Concrètement :

  • 1 lookup DNS = 1 thread libuv occupé
  • 4 lookups simultanés = pool saturé
  • les autres opérations (DNS, fs, crypto, I/O) doivent attendre

Sous charge, cela peut provoquer :

  • une augmentation de la latence
  • des timeouts intermittents
  • des erreurs difficiles à reproduire

2. Le cache DNS de l’OS n’est pas suffisant

On pourrait penser que le cache DNS du système règle le problème.
En pratique :

  • il est global, partagé entre tous les process
  • il a souvent un TTL court
  • il n’est pas conçu pour absorber des rafales de requêtes backend

Dans un backend Node.js, plusieurs requêtes concurrentes peuvent déclencher des lookups DNS identiques en parallèle, avant même que le cache OS ne soit exploité.

Exemple concret : un service externe révèle le problème

En observant le trafic DNS avec :

tcpdump -vvv -n -i any udp port 53

On peut voir défiler de nombreuses requêtes DNS vers des services tiers (monitoring, analytics, APIs externes), souvent en paires A / AAAA, répétées plusieurs fois par seconde.
Le problème n’est pas le service externe.
Le problème, c’est que Node.js refait un lookup DNS à chaque requête HTTP.

Solutions possibles... et leurs limites

Modifier /etc/hosts

Coder l’IP directement dans /etc/hosts permet une résolution instantanée.

Avantages :

  • lookup immédiat
  • zéro requête DNS

Inconvénients :

  • IP figée
  • casse lors d’un changement d’infrastructure
  • maintenance complexe
  • inadapté à une production scalable

Utiliser un resolver DNS optimisé (Unbound)

Un resolver comme Unbound est une excellente brique infra :

  • cache DNS local
  • latence très faible
  • réduction des appels vers les DNS publics
  • meilleure résilience

Mais... Node.js continue d’appeler dns.lookup() à chaque requête HTTP.

Un resolver optimisé accélère les DNS, mais ne réduit pas le nombre de lookups, ni la pression sur le thread-pool libuv.

La vraie solution : le cache DNS côté application

Pour corriger le problème à la source, il faut agir au niveau applicatif.
La solution la plus simple et propre consiste à utiliser cacheable-lookup.
Cette bibliothèque :

  • intercepte les requêtes HTTP sortantes
  • met en cache les résolutions DNS en mémoire
  • respecte les TTL DNS
  • évite les lookups répétitifs

6 lignes de code pour corriger le problème

import http from 'node:http';
import https from 'node:https';
import CacheableLookup from 'cacheable-lookup';

const cacheable = new CacheableLookup();

cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);

À partir de là :

  • un domaine n’est résolu qu’une seule fois
  • les threads libuv sont libérés
  • la latence devient plus stable
  • les timeouts chutent drastiquement

Bénéfices en production

  • APIs plus rapides
  • meilleure scalabilité sous charge
  • thread-pool libuv moins sollicité
  • moins d’erreurs réseau
  • réduction des appels DNS inutiles

Bonnes pratiques

  • respecter les TTL DNS
  • tester sous charge avant mise en production
  • combiner cache DNS applicatif + resolver DNS optimisé pour une infra robuste

Conclusion

Les performances Node.js ne se jouent pas uniquement dans le JavaScript.
Elles se jouent aussi dans l’invisible :

  • DNS
  • thread-pool libuv
  • I/O
  • cache

Parfois, quelques lignes de code suffisent à éliminer un bottleneck critique et à éviter de sérieux problèmes en production.

  • Performance
  • Code Quality
  • Administration système
Jérôme Musialak

Jérôme Musialak

CEO @ Enodo

Développeur passionné depuis l'âge de 11 ans, Jérôme a fait ses armes chez MinuteBuzz puis en tant que CTO de MeltyGroup, où il a piloté l'infrastructure technique d'une trentaine de sites à fort trafic incluant Virgin Radio, L'Étudiant, ... et les médias du groupe. Fort de cette expérience et conscient des défis récurrents rencontrés par les entreprises pour créer et diffuser efficacement leur contenu, il fonde Enodo (du latin "dénouer les nœuds") avec pour mission de simplifier l'écosystème digital. Expert en optimisation de performance et en architecture haute disponibilité, il met son obsession du détail technique au service des enjeux business pour construire des systèmes fiables qui permettent à chacun de dormir tranquille.

Sur le même thème