La aplicación se reinicia sola: errores de falta de memoria (OOM) explicados

Un error de falta de memoria (OOM) significa que tu aplicación pidió más RAM de la que su límite permite, y el runtime o el kernel la detuvo. Lee el log, encuentra qué crece y corrige la fuga o sube el límite.

Actualizado el 7 min de lectura

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
En esta página

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.

RAMtiempolímite de memoriauso establefugaOOM killreinicio
El uso estable no cae. Una fuga sube hasta el límite, el proceso muere y el ciclo vuelve a empezar en cada reinicio.

¿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í:

text
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

La 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

js
// 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:

bash
node --max-old-space-size=400 --heapsnapshot-near-heap-limit=2 index.js

Abre 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:

python
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:

bash
java -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps/ -jar app.jar

Abre 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-size en 1536 para dejar espacio a otros usos. Aplica la misma proporción a tu límite. En Java, fija -Xmx de 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

Más información

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.

Siguiente artículoQué es Docker y para qué sirveDocker es una plataforma que empaqueta una aplicación y sus dependencias en una imagen y la ejecuta como contenedor. Mira cómo funciona, un ejemplo y sus límites.

Más en Deploy

Ver los 33 artículos