Cuando se ejecuta Puppeteer en producción durante semanas, el patrón es predecible: la memoria residente del contenedor sube lentamente hasta que el kernel invoca al OOM killer. Muchos equipos culpan a una fuga concreta en Node.js, pero la realidad es más compleja. La fuga de memoria no está en tu proceso Node, sino en los procesos hijos de Chromium que no se cierran correctamente, en el estado acumulado de un navegador que vive demasiado y en procesos huérfanos que el sistema operativo sigue contando. Este artículo aborda las tres causas, las soluciones prácticas y, sobre todo, el momento en que tiene más sentido delegar la ejecución del navegador a un servicio externo, liberando a tu infraestructura de esa carga. En Q2BSTUDIO, empresa de desarrollo de software a medida, hemos visto este escenario en decenas de proyectos y sabemos que el enfoque correcto combina higiene de código, límites de concurrencia y una evaluación honesta de si realmente necesitas Chromium.
Las tres capas del problemaLa primera trampa es medir solo process.memoryUsage(). Ves el heap de Node plano y crees que no hay fuga, mientras la RSS del contenedor sube sin control. Esa diferencia es la historia completa. Chromium ejecuta un árbol de procesos: el navegador principal, los renderizadores por página y los procesos de GPU. Cuando no cierras una página —porque una excepción, un timeout o un retorno temprano se saltan el finally— ese proceso renderizador queda retenido. Multiplica eso por miles de capturas y tendrás una fuga evidente. La segunda capa es el navegador de larga duración. Mantener una instancia única de Chromium durante toda la vida del contenedor parece eficiente, pero tras miles de navegaciones acumula cachés, nodos DOM desconectados y listeners que nunca se liberan del todo. La tercera capa son los procesos zombie: una captura que cuelga o crashea deja un proceso Chromium huérfano que el cgroup sigue contando, aunque tu aplicación no lo sepa. El resultado es un contenedor que dice 'sin memoria' mientras tu app afirma usar muy poca.
Soluciones prácticas: cerrar, reciclar, acotar y reaparecerLa corrección más inmediata es envolver cada captura en un bloque try/finally. No importa si la función lanza una excepción, alcanza un timeout o retorna con éxito: la página debe cerrarse. El finally asegura que page.close() se ejecute siempre. El segundo paso es tratar el navegador como desechable. En lugar de mantener uno inmortal, recíclalo tras un número fijo de trabajos (por ejemplo, 200 capturas). Así amortizas el coste de arranque sin dejar que el estado se acumule. El tercer grupo de medidas acota el radio de explosión: lanza Chromium con --disable-dev-shm-usage para evitar el pequeño /dev/shm de los contenedores, limita el heap de V8 con --js-flags='--max-old-space-size=512', y pon un tope de concurrencia acorde a la RAM disponible. Por último, ejecuta tu proceso bajo un init (--init en Docker o tini) para que los huérfanos sean reaped, y fuerza un kill tras un timeout global para que ninguna página colgada deje un Chromium vivo para siempre.
Cómo identificar el origen realDeja de adivinar. Mide la RSS total del contenedor y el número de procesos chrome. Si ambas suben mientras el heap de Node se mantiene estable, el problema está en páginas sin cerrar, en el navegador no reciclado o en zombies. Un contador simple de páginas abiertas vs. cerradas te dirá si estás perdiendo páginas. También puedes tomar dos snapshot del heap de un contexto de navegador antes y después de una hora de tráfico representativo y buscar nodos DOM desconectados o listeners retenidos. Esa traza te lleva directo a un manejador page.on(...) que olvidaste eliminar o a la página web que estás capturando. En entornos cloud como AWS o Azure, donde cada servicio compite por recursos, una fuga así puede disparar los costes de escalado. Por eso en Q2BSTUDIO integramos prácticas de cloud AWS/Azure y monitorización de procesos para detectar estas fugas antes de que lleguen a producción.
¿Y si no necesitas Chromium en absoluto?Aquí llega la pregunta incómoda. Si tu único objetivo es obtener una imagen de una URL, todo este esfuerzo —higiene de memoria, reciclado de procesos, reaping de zombies, tuning de contenedores— es infraestructura para una tarea que podría resolverse con una petición HTTP a una API de captura de pantalla. Los servicios alojados ejecutan Chromium por ti y te devuelven la imagen directamente, sin que tengas que lanzar, cerrar o vigilar nada. El coste operativo desaparece: no hay procesos que reaper, no hay gráficos de memoria que mirar, no hay páginas sin cerrar. Para equipos que desarrollan aplicaciones a medida, inteligencia artificial o agentes de IA que necesitan entender visualmente una web, externalizar la captura permite centrarse en la lógica de negocio y en la integración con sistemas de BI/Power BI o ciberseguridad. En Q2BSTUDIO hemos ayudado a clientes a migrar desde un clúster de Puppeteer auto-gestionado a una arquitectura donde la captura es un recurso más, orquestado desde plataformas de IA o automatización. La decisión no es binaria: Puppeteer sigue siendo la herramienta correcta cuando necesitas interactuar con la página (hacer clic, rellenar formularios, extraer datos estructurados). Pero si solo quieres una foto, la solución más duradera es no ejecutar el navegador tú mismo.
Conclusión: tres problemas, un abrigo, y una salidaLas fugas de memoria de Puppeteer en producción no son un único bug: son páginas sin cerrar, un navegador que no se recicla y procesos zombies. Se corrigen con finally, reciclado, límites de concurrencia y un init process. Pero después de aplicar todo eso, todavía merece la pena preguntarse si el esfuerzo de mantener una flota de Chromium es proporcional al valor que aporta. Cuando la respuesta es 'solo necesito una captura', la solución más robusta es delegar. Así dejas de vigilar la RSS y empiezas a construir sobre una base que no se fuga.




