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 53On 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.