Kubernetes Cost Basics
This page is a hands-on tour of where Kubernetes spend actually lands, with the commands and manifests to see each line item for yourself.
Prerequisites
- kubectl matched to your cluster (Kubernetes 1.36.2).
- Metrics Server installed so
kubectl topworks. - Helm 3 for installing cost tooling later.
- Read access to your cloud billing console for load balancer, disk, and egress charges.
Quick check that metrics are flowing:
kubectl top nodes
kubectl top pods -AIf those return numbers, you can compare real usage against reservations.
Basic Examples
1. See a node's allocatable capacity
The allocatable figure, not the raw hardware, is what the scheduler can hand out.
kubectl describe node <node-name> | grep -A6 "Allocatable"- Allocatable is total capacity minus reservations for the kubelet, OS, and eviction thresholds.
- Bin-packing works against allocatable, so this is your real budget per node.
- The gap between capacity and allocatable is normal system overhead you still pay for.
- Compare this across node types to find the best price per allocatable core.
2. Read a pod's requests and limits
Requests reserve capacity; limits cap it.
kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}'- Requests are subtracted from node allocatable whether or not the pod uses them.
- A missing memory request means the scheduler treats it as zero and can overcommit the node.
- Limits above requests allow bursting into shared headroom.
- No limits at all lets one pod starve neighbors.
3. Find the reserved-versus-used gap
This gap is idle headroom, and it is pure waste.
kubectl describe node <node-name> | grep -A8 "Allocated resources"- The "Requests" column shows reserved capacity across all pods on the node.
- Compare it to
kubectl top nodeto see actual burn. - A node reserved at 95 percent but burning 30 percent is a right-sizing target.
- Consistent gaps across nodes signal cluster-wide over-requesting.
4. Count your load balancers
Each LoadBalancer service is a billed cloud load balancer.
kubectl get svc -A --field-selector spec.type=LoadBalancer- Every row is typically one provisioned, hourly-billed load balancer.
- Many rows often means many teams each exposing services directly.
- Consolidating behind an Ingress or Gateway controller collapses these.
- Internal-only services rarely need a public load balancer at all.
5. Inspect persistent volume spend
Disks bill by provisioned size, not by bytes used.
kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,SIZE:.spec.resources.requests.storage- A 100 GiB claim storing 5 GiB still bills for 100 GiB.
- Released volumes with a
Retainpolicy keep billing after the pod is gone. - Check the StorageClass tier - premium SSD costs far more than standard.
- Snapshots add their own storage line item.
6. Right-size a request from real usage
The core basic recipe: set requests near observed usage.
resources:
requests:
cpu: "150m"
memory: "256Mi"
limits:
memory: "256Mi"- Base the request on the p95 of real usage from
kubectl top, not a guess. - Set the memory limit equal to the memory request to avoid OOM surprises.
- Leave CPU without a hard limit so pods can burst into idle headroom.
- Tighter requests let more pods share each node.
7. Label workloads for cost attribution
Labels are what any cost tool groups spend by.
metadata:
labels:
team: payments
app: checkout
environment: prod- Consistent labels turn a flat bill into per-team, per-app showback.
- Enforce them with a policy engine so untagged spend cannot hide.
namespace,team, andenvironmentare the highest-value dimensions.- Without labels, OpenCost and Kubecost cannot split shared node cost.
Intermediate Examples
8. Cap a namespace with a ResourceQuota
Quotas stop one team from silently reserving the whole cluster.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: payments
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
services.loadbalancers: "2"- The quota caps total reserved CPU and memory for the namespace.
- Capping
services.loadbalancersprevents surprise load balancer sprawl. - Pods that would exceed the quota are rejected at admission.
- Pair quotas with a
LimitRangeso pods without explicit requests still get sane defaults.
9. Spot cross-zone traffic risk
Cross-zone egress is metered per gigabyte and easy to miss.
kubectl get pods -A -o wide | awk '{print $8}' | sort | uniq -c- This groups pods by node, which maps to availability zones.
- Chatty services split across zones generate billable inter-zone transfer.
- Topology-aware routing can keep traffic in-zone when possible.
- Watch data-heavy paths like databases and message queues most closely.
10. Estimate a node's monthly cost floor
A node running 24x7 sets a baseline you pay regardless of load.
kubectl get nodes -o custom-columns=NAME:.metadata.name,TYPE:.metadata.labels.node\.kubernetes\.io/instance-type- Multiply each instance type's hourly rate by roughly 730 hours per month.
- That product is your fixed floor before any autoscaling.
- Fewer, larger, well-packed nodes usually beat many small ones on price per core.
- This baseline is what spot and committed-use discounts reduce.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).