Objetivos de aprendizaje
Después de leer este artículo podrás:
- Distinguir un error OOM del runtime de una terminación silenciosa del kernel
- Descubrir qué está consumiendo memoria en Node.js, Python o Java
- Corregir la causa y fijar límites para que no vuelva a pasar
Tu aplicación funciona bien durante una hora, un día o una semana, y de pronto desaparece y vuelve a arrancar. El código no cambió y no hay stack trace. Ese patrón, un reinicio sin error de código, es la huella clásica de un crash por falta de memoria.
La memoria es el único recurso que no se degrada poco a poco. La falta de CPU hace la aplicación más lenta. La falta de memoria termina el proceso.
¿Qué es un error de falta de memoria (OOM)?#
Todo proceso necesita RAM para su código, sus datos y el runtime que lo ejecuta. Un contenedor tiene un límite de memoria. Cuando la aplicación intenta usar más de ese límite y el sistema no puede liberar nada, algo tiene que ceder.
El kernel de Linux lo documenta para los cgroups: si el uso de memoria de un cgroup alcanza memory.max y no se puede reducir, se invoca el OOM killer en ese cgroup. Docker lo dice en términos sencillos: por defecto, si ocurre un error OOM, el kernel mata procesos dentro del contenedor.
En la práctica, el nombre "OOM" cubre dos fallos distintos:
- OOM del runtime. El runtime del lenguaje nota que se quedó sin heap y aborta con un mensaje que puedes leer en el log.
- OOM kill del kernel. El kernel termina el proceso desde fuera. La aplicación no puede capturarlo y no escribe ningún mensaje.
¿Cómo sé si es un crash por OOM?#
Revisa estas señales, en este orden.
1. El mensaje del log. Un OOM del runtime en Node.js se ve así:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryLa parte central del mensaje varía (también puedes ver Ineffective mark-compacts near heap limit), pero termina con JavaScript heap out of memory.
Python lanza MemoryError y Java lanza java.lang.OutOfMemoryError (por ejemplo Java heap space). Si ves uno de ellos, es un OOM del runtime, y el stack trace te dice dónde ocurrió.
2. Un log que simplemente se corta. En un kill del kernel, la última línea es una salida normal y la siguiente ya es el mensaje de arranque, sin ningún error en medio. Junto con la siguiente señal, es casi concluyente.
3. Código de salida 137. Si tus herramientas muestran códigos de salida, 137 significa 128 más la señal 9 (SIGKILL), por convención del shell. Es lo que ves cuando el kernel mata un proceso de un contenedor.
4. El gráfico de memoria. Mira la RAM a lo largo del tiempo. Una línea que sube y cae casi a cero en cada reinicio indica que la aplicación llegó al límite. Si no sabes dónde encontrar esa salida, mira cómo leer los logs de tu aplicación.
Si el gráfico está plano y bajo cuando ocurre el crash, probablemente no es memoria. Busca una excepción no capturada.
¿Qué hace que una aplicación se quede sin memoria?#
De la causa más común a la menos común:
- Una fuga de memoria. Algo conserva referencias que debería soltar: un array que no deja de crecer, una caché sin límite de tamaño, un event listener que se agrega en cada petición, un map indexado por ID de usuario que nunca se limpia.
- Cargar demasiado de una vez. Leer un archivo grande entero o una tabla completa de la base de datos en memoria, en lugar de usar streams o paginación.
- Un límite que simplemente es pequeño. La aplicación está sana, pero su pico normal supera lo que se le asignó. Mira cuánta RAM y CPU necesita tu aplicación.
- Límite del runtime por debajo del límite del contenedor. Node.js y la JVM tienen sus propios topes de heap, separados del contenedor. La memoria fuera del heap (buffers, bibliotecas nativas, pilas de hilos) cuenta para el contenedor pero no para el heap, así que el proceso puede morir mientras el heap parece normal.
- Picos de tráfico. Muchas peticiones simultáneas, cada una con un objeto grande en memoria. Los bots que están en muchos servidores también lo sufren (mira cuánta RAM necesita un bot de Discord).
¿Cómo descubro qué está usando la memoria?#
Mide antes de adivinar. El objetivo es ver qué objetos crecen.
Node.js#
Registra la memoria cada pocos segundos. rss es el proceso entero y es lo que cuenta para el límite del contenedor:
// memory-log.js: cárgalo primero en tu archivo de entrada (require o import)
const mb = (n) => Math.round(n / 1024 / 1024);
setInterval(() => {
const m = process.memoryUsage();
console.log(`rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB external=${mb(m.external)}MB`);
}, 30_000).unref();Si heapUsed sube junto con rss, es una fuga en JavaScript. Si rss sube pero heapUsed se mantiene plano, revisa buffers, módulos nativos o streams grandes.
Para obtener un heap snapshot justo antes del crash, arranca Node con --heapsnapshot-near-heap-limit. La documentación de Node.js explica que escribe un heap snapshot de V8 en disco cuando el uso del heap se acerca al límite:
node --max-old-space-size=400 --heapsnapshot-near-heap-limit=2 index.jsAbre el archivo .heapsnapshot resultante en las Chrome DevTools (pestaña Memory) y ordena por retained size. Tomar snapshots también cuesta tiempo y memoria, así que úsalo para investigar, no como ajuste permanente.
Python#
La biblioteca estándar incluye tracemalloc. Inícialo temprano, toma un snapshot más tarde e imprime las líneas que más memoria asignaron:
import tracemalloc
tracemalloc.start()
leaky = [bytes(1000) for _ in range(50_000)] # simula tu carga de trabajo real
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:10]:
print(stat)El rastreo agrega sobrecarga, y un snapshot solo cubre las asignaciones hechas después de start(), así que actívalo mientras depuras.
Java#
Pide a la JVM que escriba un heap dump cuando lance OutOfMemoryError. Por defecto está desactivado:
java -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps/ -jar app.jarAbre el archivo .hprof en una herramienta como Eclipse MAT o VisualVM y busca los objetos retenidos más grandes.
¿Cómo corrijo un crash por OOM?#
Adapta la solución a la causa.
- Fuga: elimina la referencia. Pon límite a toda caché (un número máximo de entradas y un tiempo de vida), quita los event listeners que agregas y deja de guardar datos por usuario para siempre.
- Demasiados datos de una vez: usa streams para los archivos, pagina las consultas y procesa por lotes en lugar de armar una lista gigante.
- Límite demasiado pequeño: sube la memoria de la aplicación al pico medido más un 30% a 50% de margen, como explica el artículo de dimensionamiento de arriba.
- Tope del runtime desalineado: fija el tope del heap por debajo del límite del contenedor, para que el runtime falle con un error legible en lugar de un kill silencioso. En Node.js, la documentación sugiere, para una máquina de 2 GiB, fijar
--max-old-space-sizeen 1536 para dejar espacio a otros usos. Aplica la misma proporción a tu límite. En Java, fija-Xmxde forma explícita, ya que el valor por defecto se elige en tiempo de ejecución según la configuración del sistema. - Picos: limita la concurrencia, agrega una cola o mueve el trabajo pesado (procesamiento de imágenes, exportaciones grandes) a un proceso aparte.
Subir el límite solo es una solución válida cuando el uso es estable. Si el gráfico sube para siempre, un límite mayor solo hace que el crash llegue más tarde y sea más difícil de reproducir.
¿Cómo evito que vuelva a pasar?#
- Registra la memoria a lo largo del tiempo, como en los fragmentos de arriba, y revísala después de cada release.
- Fija topes de heap explícitos para que los fallos se noten.
- Haz pruebas de carga con datos realistas antes del lanzamiento, no con una base de datos vacía.
- Crea una alerta para cuando la memoria se quede por encima de un 80% del límite durante mucho tiempo, con un monitor externo de uptime o de métricas.
- Reinicia en horario fijo solo como parche mientras corriges una fuga conocida, nunca como la solución.
Cómo hacerlo en Square Cloud#
En Square Cloud, cada aplicación se ejecuta en un contenedor aislado con su propia asignación de memoria, y la RAM del plan es un pool que repartes entre tus aplicaciones. El mínimo es de 256 MB para una aplicación de fondo, como un bot, y de 512 MB para una aplicación web, y una sola aplicación puede usar hasta 32 GB.
Cuando una aplicación se queda sin memoria, se detiene y sigue detenida: el reinicio automático (activado por defecto en el deploy desde el panel) solo cubre un proceso que termina con error, así que subes la RAM y la vuelves a iniciar. Los logs en tiempo real en el panel, la CLI y la extensión de VS Code conservan la salida de antes de la parada, así que puedes leer las últimas líneas antes del crash. Las métricas de CPU, RAM y red se toman cada 5 minutos y se guardan durante 24 horas, lo que puede mostrar la forma de subida y caída de una fuga que tumba la aplicación en menos de un día.
Para corregir un límite demasiado pequeño, cambia la RAM de la aplicación en sus ajustes o mediante la API. Es escalado vertical, hecho por ti: no hay autoscaling. Las demás funciones de logs y métricas están en la página de funciones.
Deploy
Sube desde GitHub, la CLI o un ZIP y publica en segundos
Preguntas frecuentes
¿Qué significa el código de salida 137?
Por convención del shell, 137 es 128 más 9, es decir, el proceso terminó por SIGKILL. En un contenedor es la señal típica de un OOM kill, pero cualquier otra terminación forzada produce el mismo código, así que confírmalo con el gráfico de memoria.
¿Por qué no aparece ningún error en los logs cuando la aplicación muere?
Cuando el kernel mata un proceso por superar su límite de memoria, el proceso no tiene oportunidad de registrar nada. Solo ves que el log se corta de golpe y luego un reinicio.
¿Una fuga de memoria es lo mismo que un error OOM?
No. Una fuga es memoria que crece y nunca se libera. El error OOM es el resultado cuando el uso llega al límite. La fuga es una de las causas, pero una aplicación también puede llegar al límite cargando demasiados datos de una vez.
¿Basta con comprar más RAM?
Si el uso es estable y queda apenas por encima del límite, más RAM es la solución correcta. Si el uso sube sin parar hasta que la aplicación cae, más RAM solo retrasa el crash y tienes que corregir la fuga.
Más en Deploy
Ver los 33 artículos- Qué son las variables de entornoLas variables de entorno son pares clave-valor que el sistema operativo entrega a un proceso al iniciar. Aprende a leerlas en Node.js y Python sin exponer tokens.
- Qué es un deploy o despliegueUn deploy o despliegue lleva una versión de tu código a un servidor, la prepara y la pone en marcha. Conoce las etapas, los métodos y cómo hacer rollback.
- ¿Cuánta RAM y CPU necesita mi aplicación?Dimensiona RAM y CPU midiendo el pico real de la aplicación y sumando un margen del 30%. Mira cómo medir en Node.js, Python y Java y qué hacer cuando falte.
- Deploy automático con GitHub ActionsEl deploy automático con GitHub Actions envía tu código al servidor en cada push. Mira el workflow paso a paso, los secrets, las pruebas y los errores comunes.