Reduce Kubernetes costs without sacrificing reliability. Covers resource right-sizing, spot instances, cluster autoscaling, namespace quotas, cost allocation, and workload scheduling.
Kubernetes clusters consume cloud resources dynamically, but without proactive tuning, they over-provision, under-utilize, and incur unnecessary costs. Kubernetes Cost Optimization is the disciplined practice of aligning compute, storage, and networking usage with actual workloads—ensuring every pod, node, and service pays for its own value. It matters when clusters run longer than needed, when nodes are oversized for their load, when idle workloads consume memory and CPU without benefit, and when resource requests and limits are misaligned with observed usage.
Pods that are too large or too small waste resources. A pod with a memory request of 1Gi but consuming only 250Mi wastes 750Mi of memory that could serve other workloads. The standard tool for identifying such imbalances is kubectl top pods, which requires metrics to be collected by a metrics server.
kubectl top pods --all-namespaces
To collect CPU and memory metrics, the metrics server must be running and configured with a --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname flag and expose a /metrics endpoint. The metrics server must be reachable at http://metrics-server.kube-system.svc.cluster.local:8080.
The next step is to identify the gap between requested and actual usage. For each pod, compare:
resources.requests.memory in the pod specresources.limits.memory in the pod speccontainer.statuses[0].usage.memory from kubectl get pod -o jsonA common failure is assuming kubectl top pods reflects actual resource usage. The metrics server uses kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods and returns a JSON response with a items array of PodMetrics objects. Each entry includes timestamp, window, and metrics, where metrics is an array of Metric objects with name and value.
For instance, a pod may report:
{
"timestamp": "2023-10-05T10:00:00Z",
"window": "30s",
"metrics": [
{
"name": "cpu",
"value": "120m"
},
{
"name": "memory",
"value": "250Mi"
}
]
}
The value field uses a string with a unit suffix: m for milli, Mi for mebibytes, Gi for gibibytes. A value of 120m means 120 milli-CPU cores, or 0.12 CPU. Misinterpreting m as megabytes is a silent failure—CPU usage appears underutilized when it’s actually 12% of a single core.
To align requests with actual usage, use kubectl patch to update pod specs:
kubectl patch pod my-app-7c9f8d5b6b -n production -p '
{
"spec": {
"containers": [
{
"name": "app",
"resources": {
"requests": {
"memory": "256Mi"
},
"limits": {
"memory": "512Mi"
}
}
}
]
}
}' --type=merge
The --type=merge flag ensures that only the specified fields are updated, preserving existing values. A common mistake is using --type=strategic without understanding how it merges nested objects. With strategic merge patching, resources.requests.memory updates only the requests object, but resources.requests as a whole replaces the entire map.
Nodes are the physical or virtual machines that host pods. Over-provisioning nodes leads to wasted vCPU and memory costs. The goal is to ensure that each node’s CPU and memory utilization are high and consistent.
To analyze node utilization, use kubectl top nodes, which returns the same JSON format as top pods but for nodes.
kubectl top nodes
This command collects data from the metrics server at http://metrics-server.kube-system.svc.cluster.local:8080/apis/metrics.k8s.io/v1beta1/nodes.
Each node entry includes cpu, memory, and timestamp. The cpu value is in milli-CPU (m), and memory in bytes with a suffix like Mi, Gi. A node with cpu: 1200m and memory: 4.2Gi consumes 1.2 CPU cores and 4.2 gibibytes of memory.
To find underutilized nodes, compare the sum of pod resource requests to the node’s total capacity. A node with 8 vCPUs and 16Gi memory hosting pods with a total request of 4.5 vCPUs and 8Gi memory is underutilized.
Use kubectl describe node <name> to inspect node capacity and conditions. The Allocated Resources block shows:
Allocated resources:
(Total limits may be higher than available, but not more than capacity)
Resource Requests Limits
-------- -------- ------
cpu 4500m (4.5) 7200m (7.2)
memory 8192Mi (8Gi) 10240Mi (10Gi)
ephemeral-storage 500Mi 1000Mi
The Capacity section lists the node’s total resources:
Capacity:
cpu: 8
memory: 16384Mi (16Gi)
ephemeral-storage: 100Gi
A node is underutilized if both CPU and memory usage are below 70% of capacity and have low variance across a 15-minute window.
To reduce node count and cost, apply horizontal pod autoscaling (HPA) and cluster autoscaling (CA). The HPA uses the autoscaling/v2 API, defined in a HorizontalPodAutoscaler manifest:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
The averageUtilization field sets the target across all replicas. If cpu and memory are both below their thresholds, the HPA scales up. The HPA controller polls the metrics server every 30 seconds and updates the deployment’s replicas field.
For cluster autoscaling, configure a ClusterAutoscaler object in the cluster-autoscaler namespace. The controller watches for un-schedulable pods and node pool metrics.
A common failure is not setting scaleDownUnneededTime and scaleDownDelayAfterAdd. Without scaleDownUnneededTime, nodes are scaled down too quickly after becoming idle. Without scaleDownDelayAfterAdd, newly added nodes are scaled down before they stabilize.
The ClusterAutoscaler uses a scaleDown algorithm that considers:
A silent failure occurs when the autoscaler adds a node but the pod fails to schedule due to missing node selectors or taints. The kubectl get events command reveals:
kubectl get events -n kube-system --field-selector=involvedObject.kind=Node
Type Reason Age From Message
---- ------ ---- ---- -------
Normal NodeScheduled 2m cluster-autoscaler Successfully assigned pod-abc to node-123
Warning FailedScheduling 1m scheduler 0/10 nodes are available: 10 No resources.
The FailedScheduling event indicates that the scheduler could not place the pod on any node—often due to missing taints or required node labels.
Requests and limits define how Kubernetes schedules and manages pods. Requests determine where a pod is scheduled; limits define how much resource the pod can consume and how it is throttled.
A pod with resources.requests.cpu: 200m is scheduled to a node with at least 200 milli-CPU available. A node with 4 vCPUs (4000m) can host 20 such pods before reaching capacity.
Limits determine the maximum resource usage and how the kubelet applies CPU and memory throttling. For CPU, the cgroup controller uses a cpu.shares value. A pod with limits.cpu: 500m has cpu.shares: 500 in its cgroup.
A common mistake is setting limits.cpu without setting requests.cpu, leading to inconsistent scheduling and performance.
Use kubectl describe pod <name> to inspect the cgroup and memory limits:
kubectl describe pod my-app-7c9f8d5b6b -n production
The Status section includes Containers with Resource Requests and Resource Limits, and the Events section shows:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5s default-scheduler Successfully assigned my-app-7c9f8d5b6b to node-123
Normal Pulled 4s kubelet Container image "my-app:v1.2" already present on machine
Normal Created 3s kubelet Created container app
Normal Started 2s kubelet Started container app
Warning MemoryPressure 1s kubelet Pod memory usage exceeds threshold
The MemoryPressure event is critical: when pod memory usage exceeds limits.memory, the kubelet applies memory throttling and may start evicting pods.
To detect memory pressure, use kubectl top pod --containers and monitor container.statuses[0].usage.memory over time. A pod that consistently uses 90% of its limit but spikes to 110% triggers MemoryPressure.
The most subtle failure is when requests.memory is set too low. A pod with requests.memory: 256Mi but consuming 500Mi is scheduled to a node with only 256Mi available, but the pod’s actual memory usage exceeds the request. The node’s Allocated Resources show:
Allocated resources:
memory: 256Mi (256Mi) 512Mi (512Mi)
Here, 256Mi (256Mi) means the sum of requests.memory is 256Mi, but the limits.memory sum is 512Mi. This discrepancy causes memory contention and page faults.
To fix this, increase requests.memory to match observed usage. Use kubectl patch with a JSON merge patch:
kubectl patch deployment my-app -n production -p '
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app",
"resources": {
"requests": {
"memory": "512Mi"
},
"limits": {
"memory": "1Gi"
}
}
}
]
}
}
}
}' --type=merge
The --type=merge ensures that only resources in the container are updated, preserving existing image, ports, and env.
Pods that run longer than necessary incur idle costs. A batch job that runs for 30 minutes but is scheduled for 8 hours wastes 7.5 hours of CPU and memory.
To optimize lifecycle, use kubectl apply with a Pod manifest that includes activeDeadlineSeconds and tolerations for node taints.
apiVersion: v1
kind: Pod
metadata:
name: data-processor
namespace: batch
spec:
containers:
- name: processor
image: data-processor:v1
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 200m
memory: 512Mi
activeDeadlineSeconds: 1800
nodeSelector:
role: batch
tolerations:
- key: "node-role.kubernetes.io/batch"
operator: "Exists"
effect: "NoSchedule"
tolerationSeconds: 3600
The activeDeadlineSeconds field sets the maximum time a pod can run. After 1800 seconds (30 minutes), the pod is terminated, even if it has not finished.
The nodeSelector ensures the pod runs on nodes labeled role: batch. The tolerations allow the pod to tolerate the node-role.kubernetes.io/batch taint, which is applied by the node-controller to all batch nodes.
When the pod is scheduled and runs, the kubectl get pod data-processor -o wide shows:
NAME READY STATUS RESTARTS AGE IP NODE
data-processor 1/1 Running 0 5m 10.244.1.5 batch-node-1
The STATUS field changes to Completed when the pod finishes. After activeDeadlineSeconds, it transitions to Failed with Reason: DeadlineExceeded.
To reduce idle costs, set imagePullPolicy: IfNotPresent and livenessProbe with a failureThreshold of 3.
livenessProbe:
exec:
command:
- /bin/sh
- -c
- cat /tmp/healthy
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
The failureThreshold of 3 means the pod is considered failed after three consecutive probe failures. If the probe fails every 10 seconds, the pod is marked as unhealthy after 30 seconds of failure.
When a pod fails, the kubectl describe pod data-processor shows:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 5s kubelet Container image "data-processor:v1" already present on machine
Normal Created 4s kubelet Created container processor
Normal Started 3s kubelet Started container processor
Warning Unhealthy 25s kubelet Liveness probe failed: Get http://10.244.1.5:8080/health: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Normal Killing 15s kubelet Container processor failed liveness probe, will be restarted
Warning Failed 10s kubelet Pod failed to reach activeDeadlineSeconds
The Failed event with Reason: DeadlineExceeded indicates that the pod was killed due to exceeding the deadline.
Kubernetes Cost Optimization is not a one-off task but an ongoing discipline. It begins with identifying misaligned resource requests and limits, continues with tuning node sizing and autoscaling, and culminates in aligning pod lifecycles with workload patterns. The tools are simple: kubectl, kubectl top, kubectl patch, and kubectl describe. The data flows through standard HTTP endpoints, JSON responses, and well-documented Kubernetes APIs. The patterns are universal: request vs. limit, activeDeadlineSeconds, horizontal and cluster autoscaling, and probe-based health checks. The failures are subtle: miscalculated cpu.shares, misinterpreted memory units, and unhandled tolerations. Mastering these patterns enables teams to maintain clusters that are both efficient and resilient.
This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy