Containers Advanced

Troubleshooting Kubernetes Pod OOMKilled: Resource Limits Exceeded on Ubuntu 20.04 LTS

Resolve Kubernetes Pod OOMKilled errors on Ubuntu 20.04 LTS. Learn to diagnose and adjust resource limits (CPU/memory) to prevent applications from exceeding allocated resources and crashing.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Resolve Kubernetes Pod OOMKilled errors on Ubuntu 20.04 LTS. Learn to diagnose and adjust resource limits (CPU/memory) to prevent applications from exceeding allocated resources and crashing.

Introduction

In the dynamic world of containerized applications, an "OOMKilled" error in Kubernetes signifies a critical resource contention issue: your application pod has attempted to consume more memory than it was allocated, leading to its abrupt termination by the kernel's Out Of Memory (OOM) killer. This guide provides a comprehensive, highly technical approach to diagnose and resolve OOMKilled errors for Kubernetes Pods running on Ubuntu 20.04 LTS worker nodes, a common scenario in web hosting and DevOps environments. Understanding and correctly configuring resource limits is paramount for stable, performant, and reliable application deployments.

Symptom & Error Signature

When a Kubernetes Pod is OOMKilled, its container(s) are forcefully terminated by the operating system kernel due to excessive memory consumption. Users typically observe intermittent application unavailability, service restarts, or slow responses. On the terminal, you'll see specific status messages and events indicating the OOMKilled state.

Here's how to identify the error signature:

  1. Pod Status and Last State:

    kubectl get pods -n <your-namespace>
    

    You'll often see pods repeatedly crashing and restarting:

    NAME                         READY   STATUS    RESTARTS   AGE
    my-app-pod-abcdefg-hijkl     0/1     OOMKilled 7          3h
    another-service-mnopq-rstuv  1/1     Running   0          5h
    

    For detailed information on a specific pod:

    kubectl describe pod my-app-pod-abcdefg-hijkl -n <your-namespace>
    

    Look for the Last State and Reason fields within the Containers section:

    ...
    Containers:
      my-app-container:
        Container ID:   containerd://a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
        Image:          my-registry/my-app:latest
        Image ID:       my-registry/my-app@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        Port:           80/TCP
        Host Port:      0/TCP
        State:          Running
          Started:      Thu, 01 Jan 1970 00:00:00 +0000
        Last State:     Terminated
          Reason:       OOMKilled
          Exit Code:    137
          Started:      Thu, 01 Jan 1970 00:00:00 +0000
          Finished:     Thu, 01 Jan 1970 00:00:00 +0000
    ...
    Events:
      Type     Reason        Age                   From               Message
      ----     ------        ----                  ----               -------
      Warning  OOMKilled     2m (x7 over 3h)       kubelet            Container my-app-container was OOM-killed.
      Normal   Pulled        2m (x7 over 3h)       kubelet            Container image "my-registry/my-app:latest" already present on machine
      Normal   Created       2m (x7 over 3h)       kubelet            Created container my-app-container
      Normal   Started       2m (x7 over 3h)       kubelet            Started container my-app-container
    

    The Reason: OOMKilled and Exit Code: 137 (which translates to 128 + 9, where 9 is SIGKILL) are definitive indicators.

  2. Kubernetes Events:

    You can filter events specifically for OOMKilled occurrences:

    kubectl get events --field-selector reason=OOMKilled -n <your-namespace>
    

    This will show a history of OOMKilled events across your namespace.

  3. Host OS OOM Logs (Ubuntu 20.04 LTS Node):

    If you have access to the underlying Kubernetes worker node, you can check the kernel logs for direct OOM killer reports.

    ssh user@<node-ip>
    sudo dmesg -T | grep -i "oom-killer"
    

    Example dmesg output:

    [Thu Nov  5 10:30:45 2026] my-app-container invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=999
    [Thu Nov  5 10:30:45 2026] CPU: 3 PID: 12345 Comm: my-app-container Not tainted 5.4.0-nn-generic #nn-Ubuntu
    ...
    [Thu Nov  5 10:30:45 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=my-app-container-id,mems_allowed=0,global_oom,task_memcg=/kubepods/burstable/pod-uuid/container-id,task=my-app-container,pid=12345,uid=1000
    [Thu Nov  5 10:30:45 2026] Memory cgroup out of memory: Killed process 12345 (my-app-container) total-vm:1024MB, anon-rss:512MB, file-rss:0MB, shmem-rss:0MB
    

    This confirms the kernel's action on the specific process within the container.

Root Cause Analysis

An OOMKilled event signals that a process within a container tried to allocate more memory than its assigned memory.limit in Kubernetes. This can stem from several underlying issues:

  1. Misconfigured Kubernetes Resource Limits:

    • Insufficient memory.limits: The most common cause. The resources.limits.memory defined in the Pod's YAML manifest is simply too low for the application's actual operational needs under typical or peak load.
    • Aggressive memory.requests: While requests primarily influence scheduling, if they are set very low, Kubernetes might schedule the pod onto a node that is already heavily utilized, increasing the chance of contention even if the limits are seemingly adequate.
    • No Limits Defined: If no memory.limits are specified, the pod can theoretically consume all available memory on the node, potentially leading to the node's OOM killer activating and indiscriminately killing processes, including other pods or even critical system services.
  2. Application Memory Leaks or Inefficiency:

    • Memory Leaks: The application itself may have a bug causing it to continuously allocate memory without releasing it, leading to a gradual increase in consumption until the limit is hit.
    • Inefficient Code/Configuration: The application might be designed inefficiently, consuming more memory than necessary for its workload (e.g., loading entire datasets into memory, poor caching, excessive object creation).
    • Misconfigured Application Runtimes: For environments like Java JVMs, Node.js, or Python, the application's internal memory management (e.g., JVM heap size with -Xmx) might be set higher than the container's memory.limit, leading to the application attempting to exceed the cgroup limit.
  3. Insufficient Node Resources:

    • Node Over-subscription: Even with correctly configured pod limits, if the Kubernetes node itself is over-subscribed (i.e., the sum of all pod requests/limits exceeds the node's physical capacity), resource contention can occur.
    • Kernel Overhead: The Linux kernel and Kubernetes components (kubelet, containerd/docker) themselves require memory, reducing the total allocatable memory for pods.
  4. Workload Spikes and Unexpected Load:

    • An sudden surge in traffic or processing load can push an application's memory usage beyond its typical baseline, exceeding configured limits.
    • Batch jobs or data processing tasks might have variable memory requirements, occasionally spiking and triggering the OOM killer.

Step-by-Step Resolution

Resolving OOMKilled errors requires a methodical approach, starting with diagnosis and moving to configuration adjustments and potential application optimization.

#### 1. Identify the OOMKilled Pod and its Resource Usage

Begin by gathering comprehensive data about the struggling pod.

  1. Get detailed pod information:

    kubectl describe pod <pod-name> -n <your-namespace>
    

    Pay close attention to the Containers section for Limits and Requests values, and the Events section for OOMKilled reports.

  2. Check current resource limits and requests: It's crucial to see what's currently configured.

    kubectl get deployment <deployment-name> -n <your-namespace> -o yaml | grep -A 5 -B 5 "resources:"
    

    (Replace deployment-name with the actual deployment name managing the pod).

  3. Monitor actual pod resource usage (if Metrics Server is installed): If the Kubernetes Metrics Server is deployed in your cluster, you can get real-time (or near real-time) usage.

    kubectl top pod <pod-name> -n <your-namespace> --containers
    

    This can show you the memory usage before the pod was killed, or its usage pattern if it restarts successfully for a brief period. Also check node-level usage:

    kubectl top node
    

    This helps identify if the entire node is under pressure.

  4. Examine node kernel logs for OOM events: SSH into the Kubernetes worker node where the affected pod was running and check dmesg.

    ssh user@<node-ip>
    sudo dmesg -T | grep -i "oom-killer|memory cgroup" | tail -n 20
    

    This confirms the kernel's perspective and can reveal the exact process ID and cgroup context of the killed container.

#### 2. Analyze Application Memory Patterns

Understanding why your application is using memory is key.

  1. Review Application Metrics: Utilize your monitoring stack (e.g., Prometheus/Grafana, Datadog, New Relic) to analyze historical memory consumption trends for the application. Look for:

    • Gradual memory increase over time (potential leak).
    • Sudden spikes coinciding with specific events or peak load.
    • High baseline memory usage.
    • Memory usage during different times of day or specific operations.
  2. Application Logs: Check application-level logs for any internal memory warnings, garbage collection issues (for Java/Go), or errors that might indicate inefficient memory handling.

  3. Profiling (if applicable): If you suspect a memory leak or inefficiency, consider running memory profiling tools on a development or staging environment. For Java, tools like VisualVM or YourKit can be invaluable. For Node.js, the built-in V8 profiler or heapdump library.

#### 3. Adjust Kubernetes Resource Limits and Requests

Based on your analysis, the primary action is often to tune the resources section of your Pod's manifest (usually defined in a Deployment, StatefulSet, or DaemonSet).

  1. Edit the Deployment/StatefulSet/DaemonSet: Use kubectl edit or directly modify your YAML files in your Git repository.

    kubectl edit deployment <deployment-name> -n <your-namespace>
    
  2. Incrementally Increase memory.limits: Start by increasing the memory.limits for the affected container. A good starting point is to increase by 25-50% based on observed peak usage, then monitor.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app-deployment
    spec:
      replicas: 3
      template:
        spec:
          containers:
          - name: my-app-container
            image: my-registry/my-app:latest
            resources:
              limits:
                memory: "768Mi" # Increased from e.g., 512Mi
                cpu: "1000m"   # (1 full CPU core)
              requests:
                memory: "512Mi" # Adjust requests accordingly
                cpu: "500m"    # (0.5 CPU core)
    

    Always increase memory.limits incrementally. Over-provisioning too much memory can lead to inefficient resource utilization across your cluster and potentially starve other pods if the node runs out of physical RAM.

  3. Adjust memory.requests:

    • memory.requests defines the minimum memory guaranteed to the container and is used for scheduling. Setting it too low can lead to pods being scheduled on nodes with insufficient actual free memory, even if limits are higher.
    • If you want to ensure the pod always gets a certain amount of memory, make requests equal to limits. This gives the pod a "Guaranteed" Quality of Service (QoS) class, which is less likely to be OOMKilled under node-level memory pressure. However, it can reduce cluster utilization flexibility.
    • A common strategy for performance-critical applications is to set requests slightly below limits (e.g., 75-80%) to allow for some burst capacity while ensuring a decent baseline.
  4. Consider cpu.limits and cpu.requests: While OOMKilled directly relates to memory, CPU throttling can sometimes indirectly impact memory usage by slowing down garbage collection or processing, leading to memory accumulation. Ensure CPU limits are also appropriately set.

    # Example for CPU limits and requests
    resources:
      limits:
        cpu: "1000m" # 1 CPU core
        memory: "768Mi"
      requests:
        cpu: "500m"  # 0.5 CPU core
        memory: "512Mi"
    
  5. Apply Changes: Save the changes, and Kubernetes will automatically roll out new pods with the updated resource configurations.

#### 4. Optimize Application Memory Usage

If increasing resource limits doesn't fully resolve the issue, or if you identify memory leaks/inefficiencies, application-level optimization is necessary.

  1. Tune Runtime Memory Settings: For applications running on specific runtimes, ensure their internal memory settings are aligned with the container's memory.limit.

    • Java (JVM): Adjust -Xmx (maximum heap size) and -Xms (initial heap size). For example, if memory.limit is 768Mi, you might set -Xmx to 600m to leave room for non-heap memory usage (e.g., native memory, thread stacks, garbage collection overhead).

      # Example for Java application in Deployment YAML
      env:
      - name: JAVA_OPTS
        value: "-Xmx600m -Xms256m"
      
    • Node.js: For older Node.js versions, max-old-space-size might be relevant for heap size. Newer versions often handle this better by default in containers.

      # Example for Node.js application
      command: ["node"]
      args: ["--max-old-space-size=600", "server.js"]
      
    • Python: Ensure libraries are used efficiently, and large data structures are managed carefully (e.g., generators for large iterations).

  2. Code Review and Dependency Updates:

    • Examine application code for obvious memory-intensive operations.
    • Update third-party libraries and frameworks, as newer versions often come with performance and memory optimizations.
  3. Caching Strategies: Implement or optimize caching to reduce repeated data fetching and processing, thereby lowering memory footprint.

#### 5. Monitor and Iterate

After making changes, continuous monitoring is crucial.

  1. Observe Pod Behavior:

    kubectl get pods -n <your-namespace> -w # Watch for status changes
    kubectl describe pod <new-pod-name> -n <your-namespace>
    kubectl logs <new-pod-name> -n <your-namespace>
    
  2. Review Metrics: Check your monitoring dashboards to see if the application's memory usage now stays consistently within the new limits and if the OOMKilled events have ceased.

  3. Iterate: If OOMKilled persists, repeat the diagnostic steps (1 and 2) and further adjust limits (3) or optimize the application (4). This process is often iterative.

#### 6. Consider Node Resource Capacity (Advanced)

If you're consistently running into OOMKilled issues across multiple pods, even after individual pod tuning, the underlying Kubernetes worker nodes might be the bottleneck.

  1. Node-Level Resource Utilization:

    kubectl top node
    

    If kubectl top node consistently shows high memory utilization, or if your monitoring stack indicates nodes are frequently at or near their capacity, it's time to consider scaling up or out.

  2. Scale Node Resources:

    • Vertical Scaling: Upgrade your existing worker node instance types to ones with more CPU and RAM.
    • Horizontal Scaling: Add more worker nodes to your cluster to distribute the workload.
  3. Check Kubelet Configuration: Ensure kubelet on your Ubuntu 20.04 nodes is correctly configured with the cgroup driver matching your container runtime (usually systemd for containerd). Inconsistencies can lead to resource management issues.

    # On the Kubernetes worker node
    cat /etc/default/kubelet # or /var/lib/kubelet/config.yaml
    

    Look for --cgroup-driver=systemd (or cgroupDriver: systemd in config file).

    Modifying kubelet configuration directly can impact cluster stability. Always backup configuration files and perform changes in a controlled environment first.

  4. sysctl for Memory Overcommit: While generally not recommended to change without deep understanding, the vm.overcommit_memory kernel parameter on the Ubuntu host can influence how the kernel handles memory allocation.

    • 0 (default): Heuristic overcommit, allows some overcommit.
    • 1: Always overcommit, allows memory requests even if not physically available.
    • 2: Never overcommit, requires strict memory availability. For Kubernetes, the default 0 is usually fine, as cgroups manage pod limits. Changing this to 2 can lead to more application allocation failures rather than OOMKills, but it can also make scheduling much harder.
    # On the Ubuntu worker node
    sudo sysctl vm.overcommit_memory
    

By diligently following these steps, you can effectively diagnose, resolve, and prevent Kubernetes Pod OOMKilled errors, ensuring the stability and performance of your containerized applications on Ubuntu 20.04 LTS.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.