OOMKilled y exit code 137 en Kubernetes: cómo arreglarlo
En pocas palabras: OOMKilled (exit code 137 = 128 + 9, SIGKILL) significa que el kernel de Linux mató tu contenedor por pasarse de resources.limits.memory. El 90% de los casos es OOM de cgroup: revisá kubectl describe pod, subí el límite de memoria o arreglá el leak de la aplicación.
Si tu pod arranca, sirve tráfico un rato y de golpe reinicia sin dejar un solo log, casi seguro estás mirando un Kubernetes OOMKilled exit code 137: el kernel de Linux mató el contenedor con SIGKILL porque pidió más memoria de la permitida. No hubo advertencia, no hubo chance de guardar nada.
OOMKilled es el estado que Kubernetes le pone a un contenedor que el kernel terminó por consumo excesivo de memoria. El exit code 137 sale de la cuenta 128 + 9, donde 9 es SIGKILL, la señal que no se puede atrapar ni ignorar. Hay dos escenarios detrás del mismo código: el contenedor pasó su propio resources.limits.memory (OOM de cgroup) o el nodo entero se quedó sin RAM física.
En 30 segundos
- 137 = 128 + 9 (SIGKILL). El kernel mató el proceso sin aviso, sin flush, sin log de despedida.
- El 90% de los casos son OOM de contenedor, según el playbook publicado el 15 de julio de 2026 en dev.to: se pasó del limit y muere solo ese contenedor.
- El otro 10% es OOM de nodo, donde el OOM killer elige una víctima que puede no ser el pod culpable.
- Se confirma con
kubectl describe pod, mirando el bloqueLast State: Terminated / Reason: OOMKilled. - Exit code 143 no es lo mismo: ese es SIGTERM, casi siempre una liveness probe que falla. Problema de probe, no de memoria.
¿Por qué tu pod muere sin dejar un solo log?
Porque SIGKILL no se negocia. Cuando un proceso supera el límite de memoria de su cgroup, el kernel no le manda una notificación amable para que libere buffers: le corta la corriente. El proceso no ejecuta handlers, no cierra conexiones, no escribe el stack trace que estás buscando. Un instante servía requests y al siguiente no existe, y el pod vuelve a levantar hasta caer en CrashLoopBackOff si el patrón se repite.
Ese silencio es la primera pista. Si la app hubiera crasheado por su cuenta, tendrías una excepción en los logs. En tus pipelines CI/CD profundizamos sobre esto.
¿Cuál es la diferencia entre OOM a nivel contenedor y OOM a nivel de nodo?
El OOM de contenedor pasa cuando un contenedor excede su propio resources.limits.memory y muere solo él, sin afectar vecinos. El OOM de nodo pasa cuando la máquina se queda sin memoria física y el OOM killer del kernel elige a quién sacrificar, que puede ser cualquier proceso, incluso uno que se estaba portando bien. El fix cambia por completo según cuál tengas, así que separarlos es el paso cero.
| OOM de contenedor (cgroup) | OOM de nodo | |
|---|---|---|
| Qué pasó | El contenedor pasó su limits.memory | El nodo se quedó sin RAM física |
| Quién muere | Solo ese contenedor | La víctima que elija el kernel |
| Frecuencia | ~90% de los casos | El resto |
| Dónde se ve | kubectl describe pod → Reason: OOMKilled | dmesg en el nodo + eventos de presión de memoria |
| Fix típico | Ajustar limits o arreglar el consumo de la app | Revisar requests del cluster y capacidad del nodo |

Ojo con el caso feo: en un OOM de nodo, el pod que murió y el pod que causó la presión pueden ser dos pods distintos. Ahí perseguís al inocente durante horas mientras el verdadero glotón sigue creciendo tranquilo.
Paso 1: ¿cómo confirmar con kubectl que el pod fue OOMKilled?
Corré kubectl describe pod <nombre> y leé el estado del contenedor terminado, no el del que está corriendo ahora. El bloque que importa es Last State. Si dice Reason: OOMKilled, ya está confirmado y no hace falta adivinar nada más.
El ejemplo del playbook de dev.to muestra exactamente esto:
Last State: Terminated
Reason: OOMKilled
Started: Wed, 15 Jul 2026 09:14:02
Finished: Wed, 15 Jul 2026 09:14:48Fijate en los timestamps: 46 segundos de vida. Ese dato solo ya te dice mucho, porque un contenedor que muere a los 46 segundos de arrancar no tiene un leak lento, tiene un limit ridículamente bajo para lo que la app carga al inicializar (JVM heap, un modelo, un cache precalentado, lo que sea). Sobre eso hablamos al configurar tu servidor de CI.
¿Y si el Reason dice otra cosa? Entonces no es memoria. Un Error con exit code 143 es SIGTERM, típicamente una liveness probe que falla y ordena el reinicio, y ahí el fix está en los thresholds de la probe. Un Evicted tampoco es OOMKilled: eso es el kubelet desalojando pods por presión de recursos antes de que el kernel intervenga.
Paso 2: ¿cómo debuggear el consumo real de memoria?
La secuencia es corta y va de afuera hacia adentro: primero mirás cuánto consume el pod ahora, después qué decía la app antes de morir, y por último qué vio el kernel. Tres comandos, cinco minutos.
kubectl top pod --containers: consumo actual por contenedor. Sirve para ver contra qué limit estás jugando y cuál de los sidecars es el que se infla.kubectl logs <pod> --previous: los logs de la instancia anterior, la que murió. Sin el flag--previousvas a leer los del contenedor nuevo y no vas a encontrar nada.dmesg | grep -i oomen el nodo: acá aparece la evidencia dura del kernel, con el proceso elegido y sus números deanon-rss(memoria realmente residente del proceso) ytotal-vm(espacio virtual reservado, casi siempre mucho más alto y casi siempre irrelevante).
La confusión clásica está ahí: mirás total-vm, ves un número enorme, entrás en pánico. El que cuenta contra tu limit es anon-rss.
¿Tenés un memory leak o los limits están mal configurados?
La diferencia se ve en la forma de la curva. Un memory leak crece de manera monótona en el tiempo, sin importar el tráfico: el pod arranca en 200 MB, a las dos horas está en 600 MB, a las seis horas revienta, y reinicia para empezar el mismo ascenso. Un limit mal calibrado, en cambio, explota en los picos y se queda tranquilo el resto del día. Te puede servir nuestra cobertura sobre entornos Kubernetes complejos.
Para verlo necesitás histórico, y kubectl top te da una foto, no la película. Prometheus con Grafana sobre la métrica container_memory_working_set_bytes es lo que te muestra la curva completa (si ya tenés stack de observabilidad, esto lo resolvés en un dashboard; si no, es la excusa perfecta para armarlo).
Si la curva sube en escalera y nunca baja después del pico de tráfico, tenés un leak. Subir el limit ahí solo te compra tiempo: pasás de morir cada 6 horas a morir cada 18, y el bug sigue vivo.
¿Cómo se arregla OOMKilled? Tres caminos, del parche al fix real
Hay tres respuestas posibles y conviene elegir según lo que te dijo el diagnóstico. Subir el limit es la rápida, arreglar el código es la correcta, y calibrar requests contra consumo medido es la que evita que vuelva.
- Subir
resources.limits.memory: válido si medís que el pico legítimo supera el limit. Es un parche honesto cuando el limit era una estimación a ojo. - Arreglar el consumo de la app: si es leak, no hay YAML que te salve. Caches sin TTL, conexiones que no se cierran y buffers que crecen sin tope son los sospechosos de siempre.
- Calibrar requests y limits con datos:
requestsigual al consumo promedio observado bajo carga,limitsentre 1,5x y 2x ese valor.
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"Ese request de 512Mi no es decorativo: es lo que el scheduler usa para decidir en qué nodo entra el pod. Si lo dejás vacío o en cero, el scheduler apila pods hasta hacer explotar el nodo, y ahí caés en el escenario de OOM de nodo, el más difícil de rastrear. La documentación oficial de Kubernetes sobre gestión de recursos es explícita sobre esta división de tareas: requests le habla al scheduler, limits protege al nodo.
¿Cómo evitar que vuelva a pasar?
Midiendo antes de configurar. La regla es simple: corré la app bajo carga real o con un stress test, observá el consumo durante un ciclo completo (incluyendo el pico), y recién ahí escribí los números en el YAML. Los defaults heredados de otro deployment son una causa raíz habitual de los OOMKilled que vas a ver. Más contexto en ejecutando agentes con límites de recursos.
Si no querés hacer esa medición a mano, el Vertical Pod Autoscaler observa el consumo histórico y recomienda requests y limits. En modo Off te da solo las recomendaciones y vos decidís, que es la forma sensata de arrancar antes de dejarlo aplicar cambios solo.
Un detalle de calibración: limits muy altos parecen la solución cómoda, pero perdés la protección del nodo, porque un pod con leak se come toda la RAM disponible y arrastra a los vecinos. Limits muy bajos y morís en cualquier pico. El punto medio se encuentra con datos, no con intuición. Y si corrés el cluster sobre nodos propios o VPS (con donweb.com o el proveedor que uses), el margen es más finito todavía que en un cloud con autoscaling de nodos, así que el cálculo importa el doble.
Errores comunes al debuggear OOMKilled
- Leer los logs del contenedor equivocado. Sin
--previousestás leyendo la instancia nueva, que todavía no hizo nada. La evidencia está en la que murió. - Confundir 137 con 143. El 143 es SIGTERM y suele venir de una liveness probe que falla. Si ajustás memoria para arreglar eso, no vas a llegar a ningún lado.
- Subir el limit sin mirar la curva. Si es leak, solo corriste el problema de lugar y ahora el pod se lleva más RAM del nodo antes de caer.
- Dejar requests vacío. El scheduler cree que el pod no consume nada, sobrecarga el nodo, y terminás con un OOM de nodo que mata pods al azar.
- Mirar
total-vmen vez deanon-rss. El espacio virtual reservado no es lo que cuenta contra tu limit.
Preguntas Frecuentes
¿Qué significa OOMKilled en Kubernetes?
OOMKilled significa que el kernel de Linux terminó el contenedor porque intentó usar más memoria de la que tenía permitida. Kubernetes registra ese estado en el campo Reason del contenedor terminado. El proceso recibe SIGKILL, así que no puede guardar estado ni escribir logs antes de morir.
¿Por qué mi pod sale con exit code 137?
Porque 137 es 128 + 9, y 9 es el número de SIGKILL. Ese código indica que el proceso fue terminado por señal, no que la app haya fallado por su cuenta. En Kubernetes, la causa casi siempre es memoria: el contenedor superó su limit o el nodo se quedó sin RAM.
¿Cuál es la diferencia entre OOMKilled y CrashLoopBackOff?
OOMKilled es la causa de la muerte, CrashLoopBackOff es la consecuencia. Cuando un contenedor muere una y otra vez, Kubernetes espacía los reinicios con backoff exponencial y muestra ese estado. Un pod puede estar en CrashLoopBackOff por OOMKilled, por un error de arranque o por config inválida: hay que mirar el Reason para saber cuál.
¿Cómo sé si el problema es del contenedor o del nodo?
Si kubectl describe pod muestra OOMKilled y el consumo del contenedor estaba cerca de su limits.memory, es OOM de cgroup. Si el limit estaba lejos y aparecen varios pods afectados en el mismo nodo, revisá dmesg en esa máquina: ahí vas a ver al OOM killer del kernel eligiendo víctimas por presión de memoria física.
¿Sirve el Vertical Pod Autoscaler para evitar OOMKilled?
Sirve para calibrar requests y limits en base a consumo histórico real, que es la causa más común del problema. Lo que no hace es arreglar un memory leak: si la app crece sin parar, el VPA va a recomendar valores cada vez más altos hasta que no haya nodo que aguante. Arrancá en modo recomendación antes de dejarlo aplicar cambios.
Conclusión
OOMKilled no es un misterio, es un diagnóstico de tres pasos: confirmás el Reason con describe, separás si fue cgroup o nodo, y mirás si la curva de memoria sube en el pico o sube siempre. Recién ahí decidís si tocás el YAML o abrís el código.
El error que más caro sale es el atajo: duplicar el limit, ver que el pod deja de reiniciar y cerrar el ticket. Funciona hasta que no funciona, y para entonces el pod se está llevando 8 GB del nodo. Medí el consumo bajo carga, poné requests en el promedio observado y limits entre 1,5x y 2x, y la mayoría de estos incidentes desaparece antes de existir.






