28 juil. 2025·7 min de lecture

EMFILE : trop de fichiers ouverts dans Node — déboguer en production

Les erreurs EMFILE too many open files dans Node proviennent souvent de handles non fermés dans des apps générées par IA. Voir les causes courantes et des vérifications rapides en production pour confirmer la correction.

EMFILE : trop de fichiers ouverts dans Node — déboguer en production

Ce que « trop de fichiers ouverts » signifie vraiment

L'erreur apparaît généralement sous la forme EMFILE: too many open files (ou ENFILE). Cela veut dire que votre application a épuisé ses descripteurs de fichiers.

Un descripteur de fichier est une petite poignée que le système d'exploitation donne à un processus quand il ouvre quelque chose comme un fichier, une socket réseau, un fichier de log ou un répertoire. Quand le processus atteint sa limite, les nouveaux opens échouent.

Cela peut casser des parties de votre application qui ne ressemblent pas du tout à des « fichiers » : appels d'API, connexions à la base, uploads, rendu côté serveur, même la lecture des fichiers de configuration. C'est pour ça que le même incident peut ressembler à des 500 aléatoires jusqu'à ce que vous trouviez le bon message dans les logs : EMFILE.

Souvent, l'apparition n'intervient qu'après des heures ou des jours parce que les fuites peuvent être lentes. Une requête peut ouvrir un fichier ou une socket et oublier de le fermer. Une fuite est invisible. Dix mille requêtes plus tard, la suivante est celle qui échoue.

Les apps Node générées par l'IA fuient plus souvent des ressources car elles assemblent des extraits sans cycle de vie clair. Les signes fréquents sont l'absence de finally, des listeners ajoutés à chaque requête, des streams sans handlers d'erreur, ou des « quick fixes » qui ouvrent des connexions au lieu de réutiliser celles existantes.

Quand ça arrive, capturez un petit instantané tout de suite. C'est souvent suffisant pour relier le pic à un code ou à un trafic spécifique :

  • la fenêtre temporelle (première erreur et pic)
  • quel endpoint, job ou worker tournait
  • version/deploy/commit et changements de config
  • une stack trace complète et les logs environnants
  • le niveau de trafic (normal, pic, ou job en arrière-plan)

Comment cela se manifeste dans les déploiements Node

EMFILE ne commence presque jamais comme une panne propre et évidente. La plupart des équipes remarquent d'abord des 500 « aléatoires », des requêtes bloquées ou un conteneur qui n'accepte soudainement plus de connexions alors que CPU et mémoire semblent normaux.

Deux formes de fuite apparaissent en production :

  • Fuites par requête : échouent vite. Un afflux de trafic déclenche des échecs en quelques minutes parce que chaque requête laisse un fichier, une socket ou un watcher ouvert.
  • Fuites lentes : échouent tard. L'app tourne pendant des heures ou des jours, puis tombe après une lente accumulation de ressources non fermées.

Dans les logs et le comportement, cela ressemble souvent à :

  • des pics de 5xx qui reviennent à la normale après un redémarrage
  • les uploads ou le traitement d'images qui échouent en premier (les streams consomment vite les descripteurs)
  • des erreurs base de données ou Redis qui semblent sans rapport (nouvelles sockets impossibles à ouvrir)
  • « ça marche en local » mais échoue sous trafic réel ou jobs cron
  • un pod est « maudit » tandis que les autres vont bien

Les redémarrages peuvent masquer la cause racine. Si votre plateforme redémarre vite les processus plantés, vous pouvez être dans une boucle : la fuite grandit, l'app meurt, l'app revient « saine », et la fuite repart.

L'autoscaling peut rendre le phénomène aléatoire. Les nouvelles instances démarrent avec un compteur de descripteurs propre, donc les erreurs disparaissent quand le trafic change de cible. Puis le même chemin de code tourne à nouveau, et seules certaines pods échouent.

Une vérification de bon sens : si l'erreur survient immédiatement au démarrage avec peu de trafic, vous touchez peut‑être simplement une limite basse de descripteurs. Si elle apparaît après le runtime et s'aggrave autour de routes ou jobs spécifiques (uploads, scraping, génération PDF), c'est généralement une fuite applicative.

Causes fréquentes dans les apps Node générées par l'IA

Les projets Node générés par IA fonctionnent souvent en démo, puis touchent EMFILE dès que le trafic réel, des fichiers réels ou des jobs longue durée apparaissent. Le schéma sous-jacent est simple : quelque chose ouvre un fichier ou une connexion et ne la ferme pas, donc le processus finit par manquer de descripteurs.

Un déclencheur courant est du code de gestion de fichiers qui utilise des streams mais ne les ferme pas sur chaque chemin. Par exemple, un handler ouvre un read stream, puis retourne tôt sur une erreur sans appeler destroy() ni attendre finish/close.

Une autre cause fréquente est la présence de watchers de fond créés par des scaffolds. Un prototype peut lancer des watchers pour le hot reload, la génération de vignettes ou des tâches de sync, puis les laisser tourner en production. Chaque watcher utilise des descripteurs et peut se multiplier à travers les workers.

Les fuites qui reviennent le plus souvent

Ceux-ci sont des coupables répétés dans le code généré par IA :

  • Streams ouverts dans des boucles (imports CSV, traitement d'images en batch) sans backpressure, donc des centaines de fichiers ouverts simultanément.
  • Clients HTTP ou sockets TCP bruts maintenus indéfiniment, surtout quand des retries créent de nouvelles connexions sans fermer les anciennes.
  • Mauvaise utilisation des pools de base de données : connexions acquises mais non libérées sur les chemins d'erreur (absence de finally).
  • Processus enfants spawnés pour conversion ou scraping, avec les pipes stdout/stderr laissés ouverts.
  • Writers de logs ou métriques qui ouvrent un nouveau fichier par requête, ou qui font mal la rotation et gardent d'anciens handles vivants.

Pourquoi le code IA aggrave le problème

Le code généré a souvent beaucoup de retours précoces et de catches, mais pas de nettoyage cohérent. Il mélange aussi des patterns (callbacks, promises, streams) dans une même fonction, ce qui rend facile d'oublier un chemin de sortie.

Moyens rapides pour confirmer qu'il s'agit bien d'une fuite de FD (pas d'une supposition)

Quand vous voyez EMFILE too many open files Node, répondez à une question : atteignez-vous une limite basse, ou votre app fuit-elle des descripteurs de fichiers au fil du temps ?

D'abord, vérifiez la limite actuelle pour le processus en cours.

# In the same environment as the Node process
ulimit -n

# Per-process limits (replace PID)
cat /proc/PID/limits | grep -i "open files"

Ensuite, mesurez combien de FD le processus Node a ouverts maintenant, puis vérifiez à nouveau plus tard. Une fuite ressemble à un nombre qui n'arrête pas de monter même quand le trafic est stable.

# Count open file descriptors for the process
ls -1 /proc/PID/fd | wc -l

Si possible, échantillonnez ou mettez ce compte en graphique. Vous cherchez une montée régulière qui ne retombe pas après la fin des requêtes.

Pour voir ce qui reste ouvert, prenez un instantané lsof et cherchez la répétition.

# High-level view of what the process is holding
lsof -p PID | head

# Quick pattern check (examples: uploads, temp files, sockets)
lsof -p PID | grep -E "(/tmp|uploads|\.log|TCP)" | head

Quelques motifs courants :

  • des milliers de noms de fichiers temporaires similaires (uploads non fermés)
  • des fichiers de log répétés (logger personnalisé qui rouvre)
  • beaucoup de sockets sortants (clients HTTP qui ne ferment pas)

Étape par étape : isoler et arrêter la fuite

Sauver votre app Node construite par l'IA
FixMyMess transforme des prototypes Node générés par IA défaillants en applications prêtes pour la production avec vérification humaine.

Traitez EMFILE comme un problème de débit : quelque chose ouvre des descripteurs plus vite qu'il ne les ferme. L'objectif est de prouver quel processus et quelle fonctionnalité font monter le compteur, puis de livrer le plus petit correctif sûr.

Commencez par caler le timing. Faites correspondre le début des erreurs avec des pics de trafic, des cron jobs, des workers ou des tâches batch. Si ça n'arrive que pendant une importation nocturne, vous avez déjà un fort suspect.

Puis regardez ce qui est réellement ouvert. Une fuite causée par des uploads aura souvent beaucoup de fichiers réguliers ouverts. Un mauvais pattern client HTTP aura beaucoup de sockets dans des états similaires. Certains setups de logging laissent des pipes ouverts.

Un flux d'isolation pratique :

  • Identifiez le PID qui génère les erreurs et surveillez le compte de FD ouverts toutes les 10 à 30 secondes.
  • Capturez un instantané lsof et scannez les chemins ou endpoints distants les plus répétitifs.
  • Désactivez un worker, job ou feature flag à la fois et observez si la courbe de FD cesse de monter.
  • Ajoutez de petits compteurs autour du chemin suspect (opens vs closes par requête/job) et loggez seulement des agrégats.
  • Déployez un correctif ciblé et confirmez que la pente est plate sous la même charge.

Pour une instrumentation temporaire, gardez-la simple et sûre. Pour une route d'upload, comptez combien de streams vous créez et combien émettent close ou end. Pour un worker de fetch, loggez combien de réponses vous commencez vs combien vous consommez intégralement.

Si la désactivation d'un worker arrête la montée en quelques minutes, vous avez probablement trouvé la fuite. Si ça continue, il peut y avoir plusieurs sources ou une librairie partagée utilisée partout.

Pièges qui entretiennent la fuite

Les problèmes EMFILE persistent quand le nettoyage n'arrive que sur le chemin heureux.

Un handle fichier, une socket réseau ou un curseur est créé, puis une exception survient, et l'étape de fermeture n'est jamais exécutée. Si vous ne testez que les succès, vous ratez la fuite.

Les coupables habituels

On les retrouve beaucoup dans le code IA qui copie des patterns mais oublie les sécurités :

  • Pas de try/finally autour de tout ce que vous ouvrez, donc les erreurs court-circuitent la fermeture.
  • Streams sans handlers error, où le chemin d'erreur évite le nettoyage.
  • Watchers de fichiers comme fs.watch ou chokidar laissés en production.
  • Création d'un nouveau client DB par requête au lieu d'utiliser un pool.
  • Un shutdown qui ignore SIGTERM ou n'attend pas le cleanup, laissant des connexions anciennes pendant les déploiements.

Un exemple concret de la douleur

Imaginez un endpoint d'upload qui lit un fichier temporaire, l'envoie vers un stockage puis supprime le fichier. Sous un timeout, l'upload échoue en cours de route. Si le code ne ferme pas le read stream dans un finally, ce handle de fichier temporaire peut rester ouvert. En répétant suffisamment, le serveur atteint la limite.

Un bon test est de déclencher volontairement le chemin d'échec (annuler la requête, simuler un timeout) et de vérifier que le compte de FD ouverts cesse de monter après la fin de la requête.

Comment lire les indices sur ce qui reste ouvert

La manière la plus rapide d'avancer est de regarder ce qui est effectivement ouvert, pas ce que vous suspectez.

Échantillonnez un processus quelques fois, à une minute d'intervalle :

  • Si le compte de FD augmente régulièrement avec peu de trafic, vous chassez une fuite.
  • Si ça saute par paliers, cherchez une tâche programmée (cron), un worker de queue ou un job de fond qui s'active, fait du travail et oublie de fermer.

Motifs qui pointent vers la source

Ce que vous voyez dans lsof réduit souvent rapidement le diagnostic :

  • Beaucoup d'entrées socket : appels HTTP sortants, connexions DB, clients Redis, webhooks, proxys ou timeouts manquants.
  • Beaucoup d'entrées pipe : processus enfants (outils PDF, conversion d'images, ffmpeg) dont stdout/stderr ne sont pas consommés ou le processus n'est pas reapé.
  • Beaucoup de fichiers réels : uploads, fichiers temporaires, fichiers de log, read streams où close ne se déclenche jamais.
  • Beaucoup de chemins similaires répétés : une boucle ouvrant le même type de ressource encore et encore.

Après avoir identifié le type dominant, faites correspondre avec le timing. Si le pic coïncide avec un tick de queue, concentrez-vous sur le code du worker, pas sur le handler web. Si les sockets dominent, vérifiez le pooling et les timeouts. Si ce sont des fichiers, vérifiez le parsing des uploads et toute utilisation de createReadStream/createWriteStream.

Atténuations sûres pendant que vous travaillez sur le correctif

Stabiliser workers et uploads
Nous auditons les uploads, travaux PDF et workers de queue où EMFILE commence généralement.

Vous aurez généralement deux pistes : maintenir le service et gagner du temps pour trouver la fuite.

Augmenter la limite d'open-file peut réduire les crashs, mais considérez-le comme un tampon temporaire. Si une fuite continue de croître, une limite plus haute ne fait que retarder la panne. Faites un changement à la fois, notez le comportement avant/après et alertez si l'utilisation des FD continue d'augmenter.

Mesures à faible risque :

  • Redémarrer rapidement, mais assurez-vous que les logs persistent assez pour le debug.
  • Marquer la santé d'une instance comme mauvaise quand l'utilisation des FD dépasse un seuil, pour la faire tourner.
  • Limiter le débit de l'endpoint ou du job qui provoque le pic.
  • Désactiver la fonctionnalité fuyante derrière un flag (uploads, traitement d'images, génération PDF) jusqu'au déploiement du correctif.

Les timeouts sont aussi un garde-fou utile. Beaucoup d'apps générées par l'IA les oublient, donc des appels HTTP lents, des requêtes DB lentes ou des consumers peuvent s'accumuler et garder les sockets ouverts plus longtemps que prévu. Mettez des valeurs par défaut raisonnables et limitez les retries.

Rendez aussi le shutdown prévisible : arrêter d'accepter de nouvelles requêtes, finir le travail en cours, fermer les agents HTTP keep-alive, fermer les pools DB et arrêter les workers.

Un exemple réaliste : le worker d'upload qui gardait des fichiers ouverts

Une équipe a livré un worker d'upload Node généré par l'IA qui acceptait des rafales de PDFs, extrayait le texte et sauvegardait les résultats. En test ça marchait, mais en production il plantait après une heure chargée avec EMFILE.

Le worker utilisait fs.createReadStream() pour chaque PDF et le pipeait dans un parseur. Sur le chemin heureux, le stream finissait et le handle fichier se fermait. Mais sur le chemin d'erreur (PDF corrompu, timeout, exception du parseur), le code retournait tôt et ne nettoyait jamais le stream. Pire, il n'écoutait pas les erreurs du stream, donc certaines erreurs n'atteignaient jamais le catch.

Ce qui a changé dans le patch

Le correctif était petit mais strict : chaque exécution devait fermer les handles même en cas d'erreur.

  • Attacher des handlers error sur chaque stream impliqué.
  • Utiliser un flux de contrôle garantissant le nettoyage (par exemple un try/finally).
  • Dans le cleanup, appeler destroy() sur les streams qui peuvent encore être ouverts.

Une version simplifiée ressemblait à ça :

const rs = fs.createReadStream(path);
try {
  await parsePdf(rs); // throws on bad PDFs
} finally {
  rs.destroy(); // safe even if already ended
}

La preuve en production que c'était corrigé

Ils ont suivi une métrique : le nombre de descripteurs de fichiers ouverts pour le processus Node.

Avant le patch, le compte de FD montait à chaque rafale et ne revenait jamais à la normale. Après le patch, il augmentait brièvement pendant les pics d'uploads puis retombait. C'était la confirmation que la fuite avait disparu.

Checklist rapide pour confirmer le correctif en production

Mettre fin à la boucle de redémarrage
Nous corrigeons la vraie cause derrière les 500 aléatoires comme les streams, sockets et watchers fuyants.

Vous n'avez pas besoin de deviner si vous avez corrigé un problème EMFILE. Vous avez besoin de quelques vérifications qui restent banales sous trafic réel.

Après le déploiement, gardez le même environnement et la même forme de charge que lorsque ça échouait (mêmes workers, mêmes jobs de fond, mêmes consommateurs de queue) :

  • Le compte de FD se stabilise au lieu de monter : de petites variations sont normales ; une montée continue ne l'est pas.
  • Plus d'EMFILE pendant un cycle de trafic complet : observez une période de pic puis un moment plus calme.
  • Les pools de connexions se stabilisent après les rafales : connexions actives et requêtes en attente doivent revenir vers la normale.
  • Le shutdown ferme vraiment les ressources : redémarrez une instance et confirmez que l'ancien processus sort proprement.
  • Une alerte simple sur les FD : alertez bien en dessous de la limite OS pour détecter rapidement les régressions.

Si le compte de FD est stable mais que les erreurs continuent, regardez les limites OS (ulimit), les sidecars ou d'autres processus sur le même hôte.

Étapes suivantes si ça continue

Si vous avez augmenté les limites et déployé un patch mais qu'EMFILE revient, supposez qu'il y a un problème structurel. Deux patterns impliquent « creuser plus » : du code qui ouvre des fichiers à de nombreux endroits sans propriétaire clair, et des boucles de fond cachées (pollers, watchers, workers) qui tournent indéfiniment et accumulent lentement des handles.

Ce qu'il faut collecter pour qu'on puisse diagnostiquer rapidement

Avant de changer plus de code, capturez un petit ensemble de preuves depuis l'environnement en échec :

  • une courte fenêtre de logs autour du premier EMFILE (avec timestamps et niveau de trafic)
  • PID, version de Node, limites du conteneur et réglage nofile en cours
  • un instantané des FD ouverts pour le PID (compte et types dominants : fichiers, sockets, pipes)
  • détails du deploy récent et ce qui a changé
  • la forme de la charge (uploads, traitement d'images, cron jobs, webhooks, consommateurs de queue)

Après ça, reproduisez avec une charge proche de la production pendant 10 à 15 minutes et observez si le compte de FD monte régulièrement. Une montée régulière signifie presque toujours une fuite.

Si la base de code est générée par l'IA et que vous ne trouvez pas rapidement le « propriétaire » de chaque stream/socket, un audit ciblé est souvent plus rapide que des patchs au hasard. FixMyMess (fixmymess.ai) est conçu précisément pour cette situation : diagnostiquer et réparer des prototypes Node générés par l'IA en traquant les fuites, renforçant les chemins de cleanup et durcissant l'app pour la production.

Questions Fréquentes

Que signifie réellement « EMFILE: too many open files » dans Node ?

Cela signifie que votre processus Node a atteint sa limite de descripteurs de fichiers. Les descripteurs de fichiers ne concernent pas que les « fichiers » : ils couvrent aussi les sockets réseau, les pipes, les répertoires et les streams, donc la panne peut se manifester par des erreurs de base de données, des appels HTTP cassés ou des 500 aléatoires.

Pourquoi EMFILE apparaît-il après des heures ou des jours au lieu de tout de suite ?

C'est généralement le signe d'une fuite : quelque chose est ouvert de manière répétée et n'est pas fermé sur tous les chemins d'exécution, surtout sur les chemins d'erreur. Si l'erreur survient immédiatement au démarrage avec peu de trafic, il se peut simplement que la limite d'open-file du processus ou du conteneur soit trop basse.

Comment savoir si c'est une fuite ou une limite OS trop basse ?

Vérifiez si le nombre de descripteurs ouverts augmente dans le temps. Si le compte monte continuellement même quand le trafic est stable, vous avez une fuite ; si le nombre est stable mais que vous avez des erreurs, la limite est probablement trop basse pour votre charge.

Quelle est la manière la plus rapide de confirmer une fuite de FD en production ?

Surveillez le nombre de FD ouverts pour le PID Node sur quelques minutes, puis échantillonnez plus tard. Une fuite ressemble à un nombre qui ne revient jamais à la ligne de base après la fin des requêtes ou des jobs, même s'il monte pendant les pics.

Quelles sont les causes les plus fréquentes d'EMFILE dans les apps Node générées par l'IA ?

Les problèmes les plus courants sont des streams non détruits sur les erreurs, l'absence de cleanup dans un finally après acquisition d'une connexion ou d'un fichier, et des watchers de fond lancés par erreur en production. Les handlers d'upload, le traitement PDF/image et les workers de queue sont des points chauds fréquents car ils ouvrent beaucoup de handles rapidement.

Que dois-je chercher dans la sortie de lsof pour trouver la source ?

Regardez ce qui reste ouvert : beaucoup de fichiers réguliers pointe vers des uploads ou fichiers temporaires ; beaucoup de sockets pointe vers des appels HTTP sortants, bases de données ou clients Redis ; beaucoup de pipes pointe vers des processus enfants. Faire correspondre le type dominant au moment du pic permet généralement de cibler une route ou un worker.

Pourquoi les redémarrages ou l'autoscaling rendent-ils EMFILE aléatoire ?

Les redémarrages réinitialisent le compte de FD, donc le service semble « réparé » pendant un moment même si la fuite persiste. L'autoscaling masque aussi le problème parce que de nouvelles instances démarrent propres pendant que seules certaines pods de longue vie accumulent suffisamment de handles fuyants pour échouer.

Que puis-je faire pour maintenir le service pendant que je corrige le problème réel ?

Arrêtez la montée des FD en désactivant temporairement le job ou la route suspecte, ou en limitant le débit du hotspot. Augmenter la limite d'open-file est une mesure tampon, pas une solution : si la fuite continue, l'incident reviendra plus tard. Redémarrez rapidement si nécessaire, mais conservez suffisamment de logs pour le debug.

Quel est le pattern de code le plus sûr pour éviter que EMFILE ne revienne ?

Rendez le nettoyage inévitable : dès que vous ouvrez un stream, un socket ou une connexion de pool, assurez-vous qu'il soit fermé dans un bloc finally ou un chemin de nettoyage garanti. Gérez aussi explicitement les erreurs de stream, car des erreurs non gérées sautent souvent le nettoyage que vous pensiez exécuter.

Comment vérifier que la correction fonctionne réellement après le déploiement ?

Suivez un signal simple : le nombre de FD ouverts pour le processus Node devrait augmenter pendant les pics puis revenir près de la ligne de base, pas monter sans fin. Si votre base de code est générée par l'IA et que vous ne trouvez pas rapidement le propriétaire de chaque stream/socket, un audit ciblé est souvent plus rapide que des patchs au hasard. FixMyMess peut proposer un audit gratuit et généralement réparer la fuite et durcir l'app en 48–72 heures.