28 jul 2025·6 min de lectura

EMFILE: demasiados archivos abiertos en Node — depúralo en producción

Los errores `EMFILE: too many open files` en Node suelen deberse a descriptores filtrados en apps generadas por IA. Revisa causas comunes y comprobaciones rápidas en producción para confirmar la solución.

EMFILE: demasiados archivos abiertos en Node — depúralo en producción

Qué significa realmente “demasiados archivos abiertos”

El error suele aparecer como EMFILE: too many open files (o ENFILE). Significa que tu app se quedó sin descriptores de archivo.

Un descriptor de archivo es un pequeño identificador que el sistema operativo le da a un proceso cuando abre algo como un archivo, un socket de red, un archivo de logs o un directorio. Cuando el proceso alcanza su límite, las operaciones de apertura fallan.

Eso puede romper partes de tu app que ni siquiera suenan a “archivos”: llamadas a APIs, conexiones a bases de datos, cargas, renderizado del lado servidor, incluso la lectura de archivos de configuración. Por eso el mismo incidente puede parecer 500s aleatorios hasta que encuentres el mensaje real en los logs: EMFILE.

A menudo aparece solo después de horas o días porque las fugas pueden ser lentas. Una petición puede abrir un archivo o socket y olvidar cerrarlo. Una fuga es invisible. Diez mil peticiones después, la siguiente petición es la que falla.

Las apps Node generadas por IA tienen más probabilidad de filtrar recursos porque suelen ensamblar fragmentos sin un ciclo de vida claro. Signos comunes son bloques finally ausentes, listeners añadidos en cada petición, streams de archivos sin manejadores de error, o “fixes rápidos” que abren nuevas conexiones en lugar de reutilizar las existentes.

Cuando ocurre, captura una pequeña instantánea de inmediato. Suele ser suficiente para relacionar el pico con código y tráfico específico:

  • la ventana de tiempo (primer error y pico)
  • qué endpoint, job o worker estaba en ejecución
  • versión/commit del deploy y cualquier cambio de configuración
  • una traza completa y logs cercanos
  • nivel de tráfico (normal, pico o trabajo en background)

Cómo se manifiesta esto en despliegues Node

EMFILE rara vez comienza como una falla limpia y obvia. La mayoría de equipos primero nota 500s “aleatorios”, peticiones atascadas o un contenedor que de repente deja de aceptar conexiones aunque CPU y memoria parezcan bien.

Dos formas de fuga aparecen en producción:

  • Fugas por petición fallan rápido. Un repentino aumento de tráfico provoca fallos en minutos porque cada petición deja un archivo, socket o watcher abierto.
  • Fugas lentas fallan al cabo de mucho tiempo. La app corre durante horas o días y luego se cae tras un goteo constante de recursos no cerrados.

En logs y comportamiento suele verse así:

  • picos de 5xx que se recuperan después de un reinicio
  • uploads o procesamiento de imágenes que fallan primero (los streams consumen descriptores rápido)
  • errores de base de datos o Redis que parecen no estar relacionados (no se pueden abrir nuevos sockets)
  • “funciona localmente” pero falla bajo tráfico real o trabajos cron
  • un pod está “maldito” mientras otros parecen bien

Los reinicios pueden ocultar la causa raíz. Si tu plataforma reinicia procesos caídos rápidamente, puedes acabar en un bucle: la fuga crece, la app muere, la app vuelve “sana” y la fuga empieza de nuevo.

El autoscaling también puede hacerlo parecer aleatorio. Las instancias nuevas empiezan con un conteo limpio de descriptores, así que los errores desaparecen cuando el tráfico se desplaza. Luego el mismo camino de código se ejecuta otra vez y solo algunos pods fallan.

Una última comprobación de sentido común: si el error ocurre inmediatamente en el arranque con poco tráfico, quizá estés simplemente alcanzando un límite de descriptores bajo. Si aparece en tiempo de ejecución y empeora en rutas o jobs específicos (uploads, scraping, generación de PDFs), suele ser una fuga en la aplicación.

Causas comunes en apps Node generadas por IA

Los proyectos Node generados por IA suelen funcionar en demos y luego alcanzan EMFILE en cuanto aparece tráfico real, archivos reales o trabajos de larga duración. El patrón subyacente es simple: algo abre un archivo o conexión y no lo cierra, así el proceso se queda sin descriptores.

Un desencadenante común es el manejo de archivos que usa streams pero no los cierra en todos los caminos. Por ejemplo, un handler abre un read stream y luego retorna temprano en caso de error sin llamar a destroy() ni esperar finish/close.

Otra causa frecuente son watchers en background creados por scaffolds “útiles”. Un prototipo puede arrancar watchers de archivos para hot reload, generación de miniaturas o tareas de sincronización y, por accidente, ejecutarlos en producción. Cada watcher usa descriptores y puede multiplicarse entre workers.

Las fugas que más aparecen

Estos son los culpables habituales en código generado por IA:

  • Streams abiertos dentro de bucles (importes CSV, procesado por lotes de imágenes) sin backpressure, de modo que cientos de archivos quedan abiertos a la vez.
  • Clientes HTTP o sockets TCP crudos mantenidos abiertos de forma indefinida, especialmente cuando los reintentos crean nuevas conexiones sin cerrar las antiguas.
  • Uso de pools de base de datos donde se adquieren conexiones que no se liberan en caminos de error (falta finally).
  • Procesos hijo spawn para conversión o scraping, con pipes stdout/stderr sin cerrar.
  • Writers de logs y métricas que abren un archivo por petición, o rotan logs incorrectamente y mantienen handles viejos abiertos.

Por qué empeora con código IA

El código generado suele tener muchos retornos tempranos y bloques catch, pero no una limpieza consistente. También mezcla patrones (callbacks, promises, streams) en una misma función, lo que facilita pasar por alto una ruta de salida.

Maneras rápidas de confirmar que es una fuga de FDs (no suposiciones)

Cuando ves EMFILE too many open files Node, responde una pregunta: ¿estás alcanzando un límite bajo, o tu app está filtrando descriptores con el tiempo?

Primero, verifica el límite actual para el proceso en ejecución.

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

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

A continuación, mide cuántos FDs tiene abiertos el proceso Node ahora mismo y vuelve a comprobar más tarde. Una fuga se ve como un número que no deja de subir incluso con tráfico estable.

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

Si puedes, muestrea o grafica ese conteo. Buscas una subida sostenida que no vuelva a bajar después de que las peticiones terminen.

Para ver qué se está dejando abierto, toma un snapshot rápido de lsof y busca repeticiones.

# 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

Algunos patrones comunes:

  • miles de nombres de archivos temporales similares (uploads no cerrados)
  • archivos de log repetidos (logger personalizado reabriendo)
  • muchos sockets salientes (clientes HTTP sin cerrar)

Paso a paso: aislar y detener la fuga

Rescata tu app Node creada por IA
FixMyMess convierte prototipos Node generados por IA en apps listas para producción con verificación humana.

Trata EMFILE como un problema de ritmo: algo abre descriptores más rápido de lo que los cierra. El objetivo es probar qué proceso y qué funcionalidad hacen que el conteo suba, y luego desplegar la corrección mínima segura.

Empieza por el tiempo. Relaciona cuándo empiezan los errores con picos de tráfico, cron jobs, workers o tareas por lotes. Si solo ocurre en una importación nocturna, ya tienes un buen sospechoso.

Luego mira qué está realmente abierto. Una fuga causada por uploads suele verse como muchos archivos regulares. Un patrón malo de cliente HTTP aparece como muchos sockets en estados similares. Algunas configuraciones de logging dejan pipes abiertos.

Un flujo práctico de aislar primero:

  • Identifica el PID que lanza errores y vigila el conteo de FD abiertos cada 10–30 segundos.
  • Captura un snapshot de lsof y escanea por las rutas o endpoints remarcados.
  • Deshabilita un worker, job o feature flag a la vez y observa si la curva de FD deja de subir.
  • Añade contadores mínimos alrededor de la ruta sospechosa (opens vs closes por petición/job) y registra solo agregados.
  • Despliega un parche dirigido y confirma que la pendiente se aplana con la misma carga.

Para instrumentación temporal, mantenlo simple y seguro. En una ruta de uploads, cuenta cuántos streams creas y cuántos emiten close o end. En un worker de fetch, registra cuántas respuestas inicias vs cuántas consumes completamente.

Si deshabilitar un solo worker detiene la subida en minutos, probablemente encontraste la fuga. Si sigue subiendo, puede que haya fuentes múltiples o una librería compartida usada en todas partes.

Trampas que mantienen la fuga viva

Los problemas EMFILE perduran cuando la limpieza solo ocurre en el camino feliz.

Un handle de archivo, socket o cursor se crea, ocurre una excepción y el paso de cierre nunca se ejecuta. Si solo pruebas cuando todo funciona, te pierdes la fuga.

Los culpables habituales

Estos aparecen mucho en código generado por IA porque copia patrones pero omite las partes de seguridad:

  • No usar try/finally alrededor de lo que abres, así los errores evitan el cierre.
  • Streams sin manejadores de error, donde la ruta de error salta la limpieza.
  • Watchers de archivos como fs.watch o chokidar activos en producción.
  • Crear un nuevo cliente de base de datos por petición en lugar de usar un pool.
  • Shutdown que ignora SIGTERM o no espera la limpieza, de modo que conexiones viejas persisten durante despliegues.

Un ejemplo concreto de cómo duele

Imagina un endpoint de uploads que lee un archivo temporal, lo envía a un almacenamiento y luego borra el temporal. Si bajo un timeout la carga falla a mitad, y el código no cierra el read stream en un finally, ese handle del archivo temporal puede quedarse abierto. Haz eso suficientes veces y el servidor alcanza el límite.

Una buena prueba es forzar el camino de fallo (cancelar la petición, simular un timeout) y verificar que el conteo de FD deja de subir después de que la petición termina.

Cómo leer las pistas de lo que queda abierto

El camino más rápido es mirar qué está realmente abierto, no qué sospechas que está abierto.

Muestra un proceso un par de veces, con un minuto de diferencia:

  • Si el conteo de FD sube de forma sostenida con poco tráfico, estás buscando una fuga.
  • Si salta en pasos, busca una tarea programada (cron), un worker de colas o un job en background que se despierta, hace trabajo y olvida cerrar.

Patrones que apuntan a la fuente

Lo que veas en lsof suele reducir la causa rápidamente:

  • Muchas entradas socket: llamadas HTTP salientes, conexiones a BD, clientes Redis, webhooks, proxies o timeouts faltantes.
  • Muchas entradas pipe: procesos hijo (herramientas PDF, conversión de imágenes, ffmpeg) donde stdout/stderr no se drenan o el proceso no se recolecta.
  • Muchos archivos reales: uploads, temporales, logs, read streams donde close nunca se dispara.
  • Muchas rutas similares repetidas: un bucle abriendo el mismo tipo de recurso una y otra vez.

Una vez identificada la clase dominante, coteja con el tiempo. Si el pico coincide con un tick de la cola, enfócate en el worker; si dominan sockets, revisa pooling y timeouts; si dominan archivos, revisa parsing de uploads y cualquier createReadStream o createWriteStream.

Mitigaciones seguras mientras trabajas en la corrección

Arregla fugas de conexiones ocultas
Diagnosticamos auth roto, pools y código de retry que mantiene sockets abiertos.

Normalmente necesitas dos vías: mantener el servicio arriba y ganar tiempo para encontrar la fuga.

Subir el límite de archivos abiertos puede reducir los crashes, pero trátalo como un colchón temporal. Si la fuga sigue creciendo, un límite más alto solo retrasa la caída. Haz un cambio a la vez, anota el antes/después y alerta si el uso de FD sigue subiendo.

Mitigaciones de bajo riesgo:

  • Reiniciar rápidamente, pero asegurando que los logs duren lo suficiente para depurar.
  • Fallar una comprobación de salud cuando el uso de FD cruce un umbral para rotar la instancia.
  • Rate-limit en el endpoint o job que provoca el pico de FD.
  • Deshabilitar la funcionalidad con fuga detrás de una flag (uploads, procesamiento de imágenes, generación de PDFs) hasta que se despliegue la corrección.

Los timeouts son otra línea de defensa útil. Muchas apps generadas por IA los olvidan, así llamadas HTTP lentas, consultas DB o consumidores pueden acumularse y mantener sockets abiertos más de lo esperado. Fija valores por defecto sensatos y limita reintentos.

También haz el shutdown predecible: deja de aceptar nuevas peticiones, finaliza trabajo en vuelo, cierra agentes HTTP keep-alive, cierra pools de BD y detén workers.

Un ejemplo realista: el worker de uploads que mantenía archivos abiertos

Un equipo desplegó un worker de uploads generado por IA que aceptaba ráfagas de PDFs, extraía texto y guardaba resultados. Funciona en pruebas, pero en producción se caía tras una hora ocupada con EMFILE.

El worker usaba fs.createReadStream() por cada PDF y lo pipeaba a un parser. En el camino feliz, el stream terminaba y el handle del archivo se cerraba. Pero en rutas de error (PDF corrupto, timeout, excepción del parser) el código retornaba temprano y nunca limpiaba el stream. Peor aún, no escuchaba errores del stream, así algunos fallos nunca llegaban al catch.

Qué cambió en el parche

El arreglo fue pequeño pero estricto: cada ejecución debía cerrar handles aunque algo fallara.

  • Adjuntar manejadores error a cada stream involucrado.
  • Usar un flujo de control que garantice la limpieza (por ejemplo, un try/finally).
  • En la limpieza, llamar a destroy() en streams que puedan seguir abiertos.

Un ejemplo simplificado fue:

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

La prueba en producción de que estaba arreglado

Rastrearon una métrica: el número de descriptores de archivo abiertos para el proceso Node.

Antes del parche, el conteo de FD subía con cada ráfaga y nunca volvía al baseline. Tras el parche, subía brevemente en picos de uploads y luego volvía a estabilizarse. Esa fue la confirmación real de que la fuga desapareció.

Lista rápida para confirmar la corrección en producción

Estabiliza workers y uploads
Auditamos uploads, trabajos de PDF y workers de colas donde suele comenzar EMFILE.

No necesitas adivinar si arreglaste un EMFILE. Necesitas unas comprobaciones que aguanten bajo tráfico real.

Después de desplegar, mantén el mismo entorno y forma de carga que cuando fallaba (mismos workers, mismos jobs background, mismos consumidores de colas):

  • El conteo de FD se estabiliza en lugar de subir: pequeñas subidas y bajadas son normales; una subida sostenida no lo es.
  • No aparecen EMFILE durante un ciclo completo de tráfico: vigila un periodo de pico y uno más tranquilo.
  • Los pools de conexión se estabilizan después de picos: conexiones activas y peticiones en cola deberían volver al baseline.
  • El shutdown cierra realmente las cosas: reinicia una instancia y confirma que el proceso viejo sale limpiamente.
  • Una alerta simple de FD: alerta mucho antes del límite del SO para detectar regresiones pronto.

Si el conteo está estable pero los errores continúan, revisa límites del SO (ulimit), sidecars u otros procesos en el mismo host.

Siguientes pasos si sigue ocurriendo

Si subiste límites y desplegaste un parche pero EMFILE vuelve, asume que hay un problema estructural. Dos patrones que suelen indicar “investiga más”: código que abre archivos en muchos sitios sin un dueño claro, y bucles background ocultos (pollers, watchers, workers) que corren para siempre y acumulan handles.

Qué recopilar para que alguien lo diagnostique rápido

Antes de cambiar más código, captura un conjunto pequeño de evidencia del entorno que falla:

  • una ventana corta de logs alrededor del primer EMFILE (con timestamps y nivel de tráfico)
  • PID, versión de Node, límites del contenedor y el nofile actual
  • un snapshot de FDs abiertos para el PID (conteo y tipos dominantes: archivos, sockets, pipes)
  • detalles del deploy reciente y qué cambió
  • la forma de la carga (uploads, procesamiento de imágenes, cron jobs, webhooks, consumidores de cola)

Después, reproduce con carga parecida a producción durante 10–15 minutos y observa si el conteo de FD sube de forma sostenida. Una subida sostenida casi siempre significa una fuga.

Si la base de código fue generada por IA y no puedes encontrar rápido el “dueño” de cada stream/socket, una auditoría focalizada suele ser más rápida que parchear a ciegas. FixMyMess (fixmymess.ai) está pensado para exactamente esta situación: diagnosticar y reparar prototipos Node generados por IA rastreando fugas, apretando rutas de limpieza y endureciendo la app para producción.

Preguntas Frecuentes

¿Qué significa realmente “EMFILE: too many open files” en Node?

Significa que tu proceso Node alcanzó su límite de descriptores de archivos. Los descriptores no son solo “archivos”; también cubren sockets de red, pipes, directorios y streams, por eso la falla puede aparecer como errores de base de datos, llamadas HTTP rotas o 500s aleatorios.

¿Por qué EMFILE aparece después de horas o días en lugar de al instante?

Normalmente apunta a una fuga: algo se abre repetidamente y no se cierra en todos los caminos, especialmente en los caminos de error. Si sucede inmediatamente al arrancar con poco tráfico, puede que simplemente tengas un límite de archivos abiertos demasiado bajo configurado para el proceso o contenedor.

¿Cómo puedo saber si es una fuga frente a un límite del SO?

Comprueba si el número de descriptores abiertos sigue subiendo con el tiempo. Si el conteo sube constantemente aunque el tráfico sea estable, estás filtrando; si el conteo está plano pero aún así obtienes errores, probablemente el límite es demasiado bajo para tu carga.

¿Cuál es la forma más rápida de confirmar una fuga de FDs en producción?

Observa el conteo de FD abiertos del PID de Node durante unos minutos y vuelve a muestrear más tarde. Una fuga se ve como el conteo que nunca vuelve a la línea base después de que las solicitudes o trabajos terminan, incluso si sube durante picos.

¿Cuáles son las causas más comunes de EMFILE en apps Node generadas por IA?

Los problemas más comunes son streams que no se destruyen en errores, falta de limpieza con finally cuando se abre una conexión o archivo, y watchers en background ejecutándose en producción por accidente. Los manejadores de uploads, procesamiento de PDFs/imágenes y workers de colas son puntos calientes porque abren muchos handles rápidamente.

¿Qué debo buscar en la salida de lsof para encontrar la fuente?

Mira qué queda abierto: muchos archivos regulares suelen apuntar a uploads o archivos temporales; muchos sockets apuntan a llamadas HTTP salientes, clientes de DB o Redis; muchas pipes suelen ser procesos hijo (herramientas de PDF/imagen). Relaciona el tipo dominante con el momento del pico para acotar a una ruta o worker.

¿Por qué los reinicios o el autoscaling hacen que EMFILE parezca aleatorio?

Los reinicios reinician el conteo de FD, así que el servicio parece “arreglado” por un tiempo aunque la fuga persista. El autoscaling también lo oculta porque las instancias nuevas empiezan limpias mientras solo algunos pods de larga vida acumulan suficientes handles filtrados para fallar.

¿Qué puedo hacer para mantener el servicio en marcha mientras trabajo en la corrección real?

Para detener el aumento de FDs, deshabilita el job o la ruta sospechosa, o aplica rate-limiting al punto caliente. Subir el límite de archivos abiertos es un amortiguador temporal, no una solución; la meta es que el conteo vuelva a estabilizarse después de que el trabajo termine.

¿Cuál es el patrón de código más seguro para evitar que EMFILE vuelva?

Haz que la limpieza sea inevitable: siempre que abras un stream, socket o conexión de pool, asegúrate de cerrarlo en un bloque finally o en una ruta de limpieza garantizada. Además maneja errores en streams explícitamente, porque los errores no controlados suelen saltarse la limpieza que esperabas.

¿Cómo verifico que la corrección es real después de desplegar?

Mide una señal simple: el conteo de FDs abiertos para el proceso Node debe subir durante picos y luego volver cerca de la línea base, no subir sin parar. Si el código fue generado por IA y no encuentras rápidamente quién posee cada stream/socket, FixMyMess puede ejecutar una auditoría gratuita y normalmente arreglar la fuga y endurecer la app en 48–72 horas.