This post is also available in: Spanish
A Pod can be Running and still have serious performance problems.
An application can become slow, continuously consume memory, restart because of OOMKilled, or experience CPU throttling even when the Kubernetes node appears to have resources available.
In this second article of our Kubernetes troubleshooting series, we will investigate:
high CPU usage;
high memory usage;
OOMKilled;CPU throttling;
Pod evictions;
node-level resource pressure.
The objective is not simply to find "which Pod is using the most resources."
We want to answer:
Is the problem in the application, its requests and limits, or the capacity of the node?
Requests, limits, and actual usage
Before investigating CPU or memory, we need to distinguish three concepts:
Resources
|
+------------+------------+
| | |
Request Limit Usage
| | |
v v v
Scheduling Enforcement Actual
Requests
A request tells Kubernetes how much of a resource a workload asks for.
For example:
resources:
requests:
cpu: "250m"
memory: "256Mi"
The scheduler uses these values when deciding which node can run the Pod.
A container can use more than its request when resources are available and its limits allow it.
Limits
A limit places a constraint on resource consumption.
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
CPU and memory limits behave differently.
CPU limit
|
v
CPU throttling
Memory limit
|
v
Memory pressure / allocation
|
v
Possible OOM kill
A container is not terminated simply because it consumes too much CPU. The kernel constrains its available CPU time through cgroup mechanisms.
Memory works differently. When memory cannot be allocated within the applicable cgroup constraint, the kernel's OOM mechanisms can terminate a process.
1. High CPU usage
Suppose an alert reports increased latency and we find a Pod consuming a large amount of CPU.
A good first check is:
kubectl top pods -A
To see individual containers:
kubectl top pod <pod> \
-n <namespace> \
--containers
We can also sort Pods by CPU:
kubectl top pods \
-n <namespace> \
--sort-by=cpu
This gives us a recent snapshot of resource consumption.
However:
kubectl topis not a replacement for a complete monitoring system.
Its values normally come from Metrics Server and are intended to provide lightweight resource signals suitable for Kubernetes components such as autoscalers.
To understand when a problem started or how it evolved over hours or days, we need historical monitoring.
What should we ask when CPU increases?
Do not immediately assume that the workload needs more CPU.
Start here:
High CPU
|
+--- Did traffic increase?
|
+--- Was the application recently changed?
|
+--- Is one endpoint CPU intensive?
|
+--- Is a dependency responding slowly?
|
+--- Is there a loop or runaway process?
|
+--- Did background workload increase?
|
+--- Is the container being throttled?
Check recent revisions:
kubectl rollout history \
deployment/<deployment> \
-n <namespace>
Then inspect resource configuration:
kubectl get deployment <deployment> \
-n <namespace> \
-o yaml
Look for:
resources:
requests:
cpu:
limits:
cpu:
CPU usage vs CPU limit
Suppose we have:
CPU request: 250m
CPU limit: 500m
CPU usage: 490m
The container is operating close to its CPU limit.
Now suppose the node reports:
Node CPU usage: 35%
That may initially look contradictory.
It is not.
The container has its own CPU constraint.
Node
16 CPUs available
|
| Plenty of CPU available
|
+------------------------------+
|
Container |
CPU limit = 500m |
| |
v |
reaches limit |
| |
v |
CPU throttling <---------------------+
A node can therefore have available CPU while an individual workload is constrained by its CPU limit.
2. CPU throttling
CPU throttling deserves its own investigation.
A workload can show:
Pods: Running
Node CPU: Normal
Memory: Normal
Errors: Low
Latency: HIGH
and still be constrained by CPU.
CPU limits are enforced through kernel and cgroup mechanisms. Once a workload exhausts its permitted CPU quota for a scheduling period, its execution can be delayed until more CPU time becomes available.
For latency-sensitive workloads, this can matter significantly.
Prometheus metrics
Depending on your container runtime and metrics collection stack, you may have metrics such as:
container_cpu_usage_seconds_total
container_cpu_cfs_throttled_periods_total
container_cpu_cfs_periods_total
container_cpu_cfs_throttled_seconds_total
The exact metrics and labels available can differ between environments, so verify what your monitoring stack actually exposes before relying on a query.
For CPU consumption, a query might look like:
rate(
container_cpu_usage_seconds_total{
namespace="production",
pod=~"api-.*",
container!="POD",
container!=""
}[5m]
)
When the CFS period metrics are available, a throttled-period ratio can be calculated with a query such as:
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total[5m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total[5m])
)
Do not interpret throttling in isolation.
Correlate:
CPU throttling
+
CPU usage
+
request rate
+
latency
+
application behavior
|
v
Better diagnosis
How should CPU throttling be resolved?
There is no universal answer.
Depending on the cause, we might:
optimize CPU-intensive code;
increase CPU requests;
modify CPU limits;
increase replicas;
adjust autoscaling;
separate background workloads;
investigate noisy neighbors;
increase node capacity.
Removing every CPU limit is not automatically the correct answer either.
Limits can help protect shared environments from workloads that would otherwise consume excessive resources.
The appropriate configuration depends on the workload's behavior and the required balance between performance and isolation.
3. High memory usage
Start again with:
kubectl top pods -A
and:
kubectl top pod <pod> \
-n <namespace> \
--containers
However, a single measurement cannot tell us whether memory looks like this:
Normal high usage
Memory
^
| ____________
| /
|______/
time
or this:
Possible leak
Memory
^
| /
| /
| /
| /
|_____/
time
Historical metrics are particularly important for memory investigations.
Questions to ask about memory
High Memory
|
+--- Is usage continuously growing?
|
+--- Does it stabilize?
|
+--- Does it correlate with traffic?
|
+--- Was the application changed?
|
+--- Is caching involved?
|
+--- Could there be a memory leak?
|
+--- Is a queue growing?
|
+--- Does the runtime have a configured heap?
|
+--- Are we approaching the memory limit?
Inspect configured resources:
kubectl get pod <pod> \
-n <namespace> \
-o yaml
For example:
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
Then compare:
Memory usage
vs
Memory request
vs
Memory limit
4. OOMKilled
One of the clearest symptoms is:
Reason: OOMKilled
Start with:
kubectl describe pod <pod> \
-n <namespace>
We can also inspect the previous container state:
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
And retrieve the previous container's logs:
kubectl logs <pod> \
-n <namespace> \
--previous
OOMKilled vs Exit Code 137
This distinction is important.
You may encounter:
Exit Code: 137
137 conventionally represents:
128 + 9 = 137
signal 9 = SIGKILL
However:
Exit code 137 by itself does not prove that the process was killed because of an out-of-memory condition.
A process can receive SIGKILL for other reasons.
Look for Kubernetes reporting:
Reason: OOMKilled
and correlate it with:
container state;
Events;
memory metrics;
logs;
node conditions.
Container OOM vs node memory pressure
Another important distinction is:
Memory problem
|
+-------+-------+
| |
v v
Container OOM Node Pressure
| |
Memory/cgroup Node running
constraint low on memory
| |
v v
OOMKilled Possible eviction
These are not the same incident.
Container OOM
For example:
limits:
memory: "512Mi"
If the workload cannot allocate the memory it needs within its applicable constraint, the kernel's OOM mechanism can kill a process.
Node memory pressure
Multiple workloads may instead consume enough memory that the node itself becomes resource constrained.
Check:
kubectl describe node <node>
Look for:
Conditions:
MemoryPressure:
And inspect current usage:
kubectl top node <node>
How should OOMKilled be resolved?
Increasing the memory limit may be correct.
But first answer:
Why does the application need more memory?
Possible explanations include:
OOMKilled
|
+--- Memory leak
|
+--- Limit too low
|
+--- Higher traffic
|
+--- Unbounded cache
|
+--- Incorrect heap configuration
|
+--- Large query / processing job
|
+--- Excessive worker concurrency
|
+--- Memory-backed volume usage
Changing:
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
to:
resources:
requests:
memory: "1Gi"
limits:
memory: "2Gi"
may be appropriate when measurements demonstrate that the additional memory is legitimate.
If the workload has a memory leak, increasing the limit may simply delay the next failure.
5. Pod evictions
Now suppose we find:
STATUS: Evicted
Start with:
kubectl describe pod <pod> \
-n <namespace>
Then investigate its node:
kubectl describe node <node>
Kubernetes supports node-pressure eviction, where the kubelet can terminate Pods to reclaim constrained node resources.
The kubelet monitors signals associated with resources such as:
Memory
Disk
Filesystem inodes
When configured thresholds are crossed, Pods can be selected for eviction so the node can reclaim resources.
Node conditions
A good starting point is:
kubectl get nodes
followed by:
kubectl describe node <node>
Look for conditions such as:
MemoryPressure
DiskPressure
PIDPressure
Then inspect current usage:
kubectl top node <node>
List workloads assigned to that node:
kubectl get pods -A \
--field-selector spec.nodeName=<node> \
-o wide
This helps identify which workloads were colocated on the affected node.
Evicted is not OOMKilled
These symptoms are easy to confuse.
OOMKilled
|
A process/container is killed
as part of an OOM condition
!=
Evicted
|
kubelet terminates a Pod
to reclaim constrained
node resources
The investigation therefore differs.
For OOMKilled, the container and its memory behavior are primary investigation targets.
For Evicted, the node and its resource pressure become especially important.
6. QoS Classes
Kubernetes assigns a Quality of Service class to Pods based on their CPU and memory resource configuration.
Check it with:
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.qosClass}'
The classes are:
Guaranteed
Burstable
BestEffort
Guaranteed
With traditional per-container resource configuration, a Pod qualifies as Guaranteed when every container has CPU and memory requests and limits, with the request equal to the limit for each resource.
For example:
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"
Burstable
A Pod with at least some CPU or memory resource configuration that does not meet the Guaranteed criteria is generally classified as Burstable.
BestEffort
A Pod without CPU or memory requests or limits is normally:
BestEffort
For example:
resources: {}
QoS and eviction
QoS matters when Kubernetes selects workloads during node resource pressure, but it is not the only consideration.
Pod priority and whether resource consumption exceeds requests can also affect eviction decisions.
Therefore, avoid reducing every eviction decision to:
BestEffort -> Burstable -> Guaranteed
as if QoS class alone completely determined the result.
The actual node-pressure eviction process considers additional factors.
7. Requests that are too low
Consider:
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "2"
memory: "2Gi"
while the workload normally consumes:
CPU: 800m
Memory: 900Mi
The Pod may run successfully.
However, its requests tell the scheduler that the workload needs considerably fewer resources than it typically consumes.
Across many workloads, this can contribute to:
Node appears to have capacity
|
Scheduler places more Pods
|
Actual usage grows
|
Node becomes saturated
|
Resource pressure
Requests should therefore not be selected solely to make Pods easier to schedule.
They should reasonably reflect the workload's resource requirements.
8. What if requests are too high?
The opposite problem also exists.
Suppose we configure:
requests:
cpu: "4"
memory: "8Gi"
for a workload that typically consumes:
CPU: 200m
Memory: 300Mi
This can waste schedulable capacity.
Even when:
kubectl top nodes
shows low actual utilization, the scheduler may refuse additional Pods because Pod placement is based on requested resources.
Inspect:
kubectl describe node <node>
and find:
Allocated resources:
This helps us compare:
Actual utilization
vs
Requested resources
9. Useful Prometheus metrics
kubectl top is useful for a quick snapshot.
During a real incident, we usually need to understand how resource behavior changed over time.
Metrics commonly available from kubelet/cAdvisor pipelines can include:
container_cpu_usage_seconds_total
container_memory_working_set_bytes
container_cpu_cfs_throttled_periods_total
container_cpu_cfs_periods_total
container_cpu_cfs_throttled_seconds_total
kube-state-metrics can separately expose Kubernetes object state such as configured requests and limits.
Exact metric availability and labels depend on the monitoring architecture.
CPU usage
For example:
sum by (namespace, pod) (
rate(
container_cpu_usage_seconds_total{
container!="",
container!="POD"
}[5m]
)
)
Memory working set
sum by (namespace, pod) (
container_memory_working_set_bytes{
container!="",
container!="POD"
}
)
Instead of only asking for the current value, inspect:
30 minutes before
|
incident begins
|
resource growth
|
failure
|
recovery
The trend is often more useful than the snapshot.
10. Correlating resources with application behavior
Consider the following timeline:
14:00 CPU normal
14:05 Deployment
14:10 CPU increases
14:12 Latency increases
14:15 CPU throttling increases
14:20 5xx increases
We now have a much stronger story than:
"CPU is high."
Our hypothesis becomes:
Deployment
|
v
CPU growth
|
v
Throttling
|
v
Latency
|
v
Errors
The next investigation might use application logs and distributed traces to discover which new execution path is consuming additional CPU.
11. Investigating the node
When the evidence suggests that the problem affects an entire node:
kubectl describe node <node>
and:
kubectl top node <node>
are useful starting points.
If deeper investigation is required and permissions allow it:
kubectl debug node/<node> \
-it \
--image=ubuntu
From the debugging environment, host resources can be inspected according to the permissions and environment available.
For example:
free -h
df -h
df -i
And in environments where administrators have direct host access, kubelet logs can also be investigated:
journalctl -u kubelet
Troubleshooting flow
We can summarize the investigation as:
Performance issue
|
v
Define impact
|
v
kubectl top / metrics
|
+---------+---------+
| |
v v
CPU Memory
| |
+------+------+ +-----+------+
| | | |
High usage Throttling Growth OOMKilled
| | | |
+------+------+ +-----+------+
| |
v v
Requests/limits Requests/limits
| |
+---------+---------+
|
v
Check node
|
+------------+-------------+
| | |
CPU usage MemoryPressure DiskPressure
| | |
+------------+-------------+
|
v
Correlate application
|
v
Logs / Metrics / Traces
|
v
Root cause
Command reference
# Current usage
kubectl top pods -A
kubectl top nodes
# Container-level usage
kubectl top pod <pod> \
-n <namespace> \
--containers
# Sort by usage
kubectl top pods \
-n <namespace> \
--sort-by=cpu
kubectl top pods \
-n <namespace> \
--sort-by=memory
# Pod state
kubectl describe pod <pod> \
-n <namespace>
# Previous container state
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
# Previous logs
kubectl logs <pod> \
-n <namespace> \
--previous
# Resource configuration
kubectl get deployment <deployment> \
-n <namespace> \
-o yaml
# Node investigation
kubectl describe node <node>
kubectl top node <node>
# Pods running on a node
kubectl get pods -A \
--field-selector spec.nodeName=<node> \
-o wide
# QoS class
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.qosClass}'
# Node debugging
kubectl debug node/<node> \
-it \
--image=ubuntu
What not to do
During an incident, avoid automatic responses such as:
OOMKilled
-> increase memory
High CPU
-> increase CPU
Evicted
-> add another node
Those actions might temporarily mitigate the problem without resolving its cause.
Instead:
Symptom
|
v
Measure
|
v
Correlate
|
v
Find constraint
|
v
Understand workload
|
v
Change
|
v
Validate
After making a change, verify:
CPU;
memory;
throttling;
restart count;
latency;
error rate;
node pressure;
application behavior.
Conclusion
Resource problems in Kubernetes can occur at multiple levels:
Application
|
Container
|
Pod
|
Node
|
Cluster
An OOMKilled container does not automatically mean the node ran out of memory.
High CPU does not automatically mean we need more CPUs.
And an evicted Pod is not the same as a process being terminated by the OOM killer.
To investigate these incidents correctly, connect:
Requests + Limits
+
Actual Usage
+
Container State
+
Node Conditions
+
Historical Metrics
+
Application behavior
|
v
Root Cause
In the next article in this series, we will troubleshoot Kubernetes networking:
Services;
EndpointSlices;
DNS;
Ingress and Gateway API;
NetworkPolicy;
CNI;
Pod-to-Pod connectivity;
outbound connectivity.
We will follow the traffic path layer by layer to determine exactly where communication is failing.
Comments