Kubernetes Cost Optimization

Reduce Kubernetes cluster costs without sacrificing reliability. Covers right-sizing pods, cluster autoscaler tuning, multi-tenancy, spot node pools, resource quotas, and the cost visibility tools purpose-built for Kubernetes.

This page is for engineers and platform operators who need to reduce Kubernetes spend without increasing risk. It matters when your cluster is growing past the point where manual kubectl get checks are sufficient, when billing alerts fire, or when you’re asked to “find savings” without a clear starting point. The focus is on actions you can take today with standard tooling, not on abstract frameworks or vendor-specific dashboards.

Identify where spend is hiding

The first step is a baseline audit. If metrics-server is installed—which it often is by default in managed clusters—kubectl top gives you a starting point. Run:

kubectl top nodes --use-metrics-server
kubectl top pods --use-metrics-server

If metrics-server is absent, kubectl describe node still reports Allocated resources: which includes requests and limits. Export that output and sum CPU/memory across all nodes to get your raw allocation figure.

Sharp edge: metrics-server aggregates per-node and per-pod, but it does not expose per-container metrics unless the kubelet is configured with the --feature-gates=CPUManager=true,MemoryManager=true flags and the Cluster Autoscaler is in use. Without those, you’ll see pod-level totals but not which container inside a multi-container pod is driving usage. A pod with two containers—one leaking memory, one idle—will mask the offender.

To drill further, use kubectl get pod <pod-name> -o json and inspect spec.containers[*].resources. Look for requests that exceed actual usage, and limits that are set so high they prevent eviction under pressure. A common pattern: requests set to match expected workload, limits set to “whatever the developer guessed,” which forces the scheduler to admit pods onto nodes that can actually honor the limit, increasing node count and cost.

Set hard boundaries with resource quotas

Once you know what’s being requested, enforce boundaries at the namespace level. A resource quota YAML looks like this:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: production
spec:
  hard:
    requests.cpu: "50"
    requests.memory: 100Gi
    limits.cpu: "80"
    limits.memory: 150Gi
    pods: "100"
    services: "20"
    persistentvolumeclaims: "20"
  scopeSelector:
    matchExpressions:
      - key: kubernetes.io/metadata.name
        operator: In
        values: ["production"]

Apply with kubectl apply -f compute-quota.yaml. The quota enforces that total requests across all pods in the namespace cannot exceed the specified caps, and total limits cannot exceed the hard caps. If a pod would push the namespace over quota, it stays in Pending status until room opens.

Sharp edge: quotas only validate against requests and limits defined in the pod spec. If a container runs without specifying resources—resources: {}—it consumes zero from the quota, but the scheduler still places it on a node that must honor the node’s capacity. Unconstrained containers are the single largest source of silent over-allocation in Kubernetes clusters.

Also note: scopes on a quota only filter by namespace label. They do not partition quota across teams or environments unless you create separate namespaces per team and bind quotas to those namespaces. If you have a shared dev namespace, a single noisy namespace can exhaust quotas for everyone.

Right-size requests and limits

With quotas in place, the next step is adjusting individual pod specs. The pattern is:

  1. Measure actual usage over a representative window (at least 7 days, covering business cycles). Use Prometheus or your existing monitoring backend. A generic query shape:
   sum by (container) (rate(container_cpu_usage_seconds_total{namespace="production"}[1h]))

If you don’t have per-container metrics, fall back to kubectl top pod averaged across the window.

  1. Set requests slightly above the 95th percentile of observed usage. This ensures the scheduler can find a node that will admit the pod without constant evictions, but doesn’t over-reserve.
  1. Set limits at the expected peak or a firm ceiling. The limit should be lower than the node’s allocatable capacity minus the sum of all other pods’ requests on that node. A common misconfiguration: setting limits equal to requests. This removes the buffer that allows the kubelet to reclaim memory under pressure, leading to OOMKilled restarts.
  1. Re-apply the pod or deployment. Use kubectl rollout restart deployment/<name> if you’re adjusting a Deployment.

Confused options: requests and limits are often set to the same value because “it’s simpler.” The result is that the scheduler treats the pod as if it guarantees both the requested and limited amount, which reduces the number of feasible placement nodes. Another frequent error: setting requests higher than limits. The kubelet will admit the pod based on the request, but the container can never exceed the limit, making the request effectively a scheduling hint rather than a reservation.

Optimize node pool composition

Even with perfect pod-level tuning, node-level waste persists. Two patterns help:

Right-sized node pools. If you have a mix of node sizes, ensure each node pool’s machine type matches the typical pod profile. A pool of n2-standard-4 (4 vCPU, 15 GB memory) running only pods with requests: 1 CPU, 2 GiB will have high unused memory. Conversely, n2-highmem-8 running CPU-heavy workloads wastes memory. Map your most common requests.cpu and requests.memory combinations to machine types, and deprecate mismatched pools.

Node problem detector and GPU/accelerator filtering. If you run accelerator workloads (GPU, TPU, FPGA), ensure they’re on dedicated node pools with nvidia.com/gpu or equivalent labels. Non-accelerator pods should have node-role.kubernetes.io/<role>: "" and be scheduled on general-purpose nodes. Mixing them causes the scheduler to reserve accelerator capacity for pods that don’t use it, or prevents accelerator pods from finding available nodes.

Sharp edge: the Cluster Autoscaler will add nodes when pods are stuck in Pending, but it does not remove nodes when pods are deleted, unless scale-down-utilization-threshold is set. A common production failure: the autoscaler adds a new m5.large node because a pod’s requests.cpu cannot be satisfied, but the new node immediately runs at 5 % utilization because the original node’s capacity was already committed to other pods. Set --scale-down-unneeded-time=10m and --scale-down-utilization-threshold=0.5 to force aggressive downsizing, and monitor with kubectl get node -o json | jq '.items[] | select(.metadata.labels."kubernetes.io/arch"=="amd64") | .status.allocatable'.

Optimize networking and storage egress

Kubernetes cost extends beyond compute. Two often-overlooked areas:

Service ingress/egress. If you’re using a cloud provider’s load balancer, each external IP incurs an hourly charge. Consolidate multiple services behind a single ingress controller where possible. For AWS, this means a single ALB shared across namespaces via ingress-classes.kubernetes.io/ingress-classes: "alb" and alb.ingress.kubernetes.io/target-type: "ip". For GCP, a single nginx-ingress or gce ingress per cluster. If you have ten clusters each with their own load balancer, you’re paying ten times the base rate.

Persistent volume reclamation. Delete reclaim policy on PVs means the underlying cloud disk is deleted when the PVC is removed. Retain means the disk persists, and you’re billed for it until you manually delete it. If you’re using dynamic provisioning (e.g., aws-ebs, gce-pd), set reclaimPolicy: Delete by default in your StorageClass, unless you have a deliberate archival requirement. A common silent failure: a developer deletes their dev namespace, the PVCs are gone, but the underlying EBS volumes remain because the StorageClass had retain: true set in a prior iteration.

Ongoing governance

Cost optimization is not a one-time audit. Establish these as recurring practices:

Sharp edge: the Horizontal Pod Autoscaler uses metrics from metrics-server. If metrics-server lags—common after a cluster upgrade or during high load—the HPA may not scale out fast enough, causing pod evictions or backoff loops. Pair HPA with kubectl get hpa -w during load tests to verify reaction time. Additionally, the HPA’s behavior field can delay scaling actions; a pause window of 5m can prevent thrashing but also delay necessary scale-out.

What this page does not cover

This page does not address cloud provider billing aggregation, tagging strategies, or commitment-based discounts (Reserved Instances, Savings Plans). Those topics belong in the “Cloud Cost Optimization” and “Cloud Commitment Strategies” pages in this wiki. It also does not cover service mesh traffic routing, service discovery latency, or ingress controller tuning beyond basic consolidation. Those are valid cost levers but require product-specific configuration that falls outside the scope of a generic Kubernetes reference.

This page was rewritten on 17 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.