DEV Community

Cover image for Kubernetes Cost Optimization 15 quick wins
Muskan
Muskan

Posted on • Originally published at zop.dev

Kubernetes Cost Optimization 15 quick wins

Why Kubernetes Bills Spiral Before Teams Notice

Kubernetes cost overruns compound in silence because the billing signal arrives weeks after the spending decision. A developer sets a memory request too high on a Tuesday. The scheduler honors that request, locks the node capacity, and the cluster quietly over-provisions from that point forward. No alert fires.

How requests create cost gaps

No ticket opens. By the time the monthly invoice lands, the pattern has replicated across a dozen services.

Visual TL;DR

Kubernetes resource requests are the scheduler's binding promise: a declared floor of CPU and memory that the control plane reserves on a node, regardless of whether the workload ever consumes it. That reservation is the gap between what you pay and what you use. When requests are miscalibrated, that gap widens with every new pod replica.

Four detection barriers

The mechanism behind cost spiral is multiplicative, not additive. One oversized Deployment costs little. The same misconfiguration copied into staging, pre-production, and production, then scaled horizontally during a load test that nobody cleaned up, produces a node count that nobody authorized. We measured this pattern in our first cluster audit: a single misconfigured resource block had propagated into 14 Deployments across 3 namespaces before anyone noticed.

diagram

Four properties make this problem structurally resistant to detection.

Billing lag. Cloud invoices reflect consumption from the prior billing cycle. By the time finance flags the number, the cluster configuration that caused it is already 30 days stale.

Request invisibility. Kubernetes does not surface the delta between requested and actual utilization in default dashboards. Teams see node count and pod health, not reservation efficiency.

Config inheritance. Helm charts and Kustomize overlays copy resource blocks from base templates. A single bad base value propagates to every environment that inherits from it, with no diff to review.

Where to start the audit

Cleanup debt. Namespaces created for feature branches, load tests, or incident debugging rarely have automated teardown. Each idle namespace holds reserved capacity at full on-demand node pricing.

The highest-leverage interventions target these four properties specifically. Fixing a single base Helm chart resource block costs one pull request. Left unfixed, that block funds idle node hours at rates that compound every sprint. Start the audit at the base templates, not the leaf Deployments.

Resource Requests and Limits: The Fastest Wins

Miscalibrated resource requests and absent namespace quotas are the two levers that produce the fastest measurable cost reduction in a Kubernetes cluster, because both are correctable with configuration changes that require no application code modification.

Kubernetes resource requests are the CPU and memory values a pod declares to the scheduler as its guaranteed floor: the control plane will not place that pod on a node unless the node has that exact capacity unallocated. The gap between the declared floor and actual runtime consumption is reserved but idle capacity. You pay for the floor, not the ceiling.

Three controls for idle capacity

The fastest path to recovering that idle capacity runs through three specific controls.

Request right-sizing. Setting requests equal to the p95 observed consumption, measured after 30 days of production traffic data, eliminates the padding developers add defensively when they lack utilization visibility. Without that data, engineers round up to the nearest "safe" number. A Java service that consumes 512Mi at p95 routinely ships with a 2Gi request because the developer recalled a past OOM event. That 1.5Gi gap, multiplied across 20 replicas, locks 30Gi of node memory that the scheduler cannot reassign.

Namespace resource quotas. A ResourceQuota object caps the total CPU and memory a namespace can request. Without one, a single misconfigured Deployment in a staging namespace consumes node capacity that production autoscaling needs. We built quota enforcement into our namespace provisioning pipeline, and in the first deployment week, runaway staging workloads dropped from a recurring problem to a non-event.

VPA in recommendation mode before enforcement. The Vertical Pod Autoscaler observes actual consumption and surfaces right-sized request values without touching running pods. Run it in recommendation mode for two weeks before enabling auto mode. Auto mode evicts and restarts pods to apply new requests, which breaks stateful workloads and disrupts jobs mid-execution if you skip the observation period.

diagram

HPA configuration deserves a separate note. Horizontal Pod Autoscaler scales replica count against a target metric, typically CPU utilization percentage. The failure mode is setting that target too low, say 30%, which causes the HPA to spin up additional replicas when the existing pods are running comfortably. Each new replica carries its own resource request, which locks more node capacity.

HPA target miscalibration

The fix is calibrating the HPA target to the p90 utilization of a correctly right-sized pod, not a padded one.

Control What it fixes Breaks when
Request right-sizing Idle reserved capacity Applied without 30-day utilization baseline
Namespace ResourceQuota Runaway staging consumption Quota set too low, blocking legitimate scale
VPA recommendation mode Surfaces correct request values Skipped before enabling auto mode on stateful pods
HPA target calibration Over-replication from low thresholds Target set against padded requests, not actual consumption

The sequence matters. Right-size requests first, then set quotas against the corrected numbers, then enable VPA auto mode on stateless workloads only. Running these steps out of order produces quota violations on correctly sized pods, which the scheduler interprets as pending and triggers cluster autoscaler to provision new nodes, directly reversing the savings you just created.

Workload Scheduling and Node Efficiency Gains

Spot and preemptible nodes, cluster autoscaler tuning, and pod affinity rules each attack idle compute from a different angle, and together they recover node spend that request right-sizing alone cannot reach.

The mechanism behind spot savings is straightforward. A cloud provider sells excess capacity at a discount, typically 60-80% below on-demand pricing, in exchange for the right to reclaim that capacity with a short eviction notice. An m5.xlarge on-demand instance runs at roughly $185/month. The same instance class on spot pricing drops that figure below $60/month.

Spot node savings and limits

Across a 30-node batch processing tier, that delta is real money recovered without touching a single line of application code. The failure condition is equally clear: spot nodes break stateful workloads and long-running jobs that cannot tolerate mid-execution interruption. Route only interruption-tolerant pods to spot capacity.

diagram

The default unneeded time is 10 minutes. In practice, we measured clusters holding idle nodes for 45 minutes or longer because a single low-priority pod with no resource request kept the node above the utilization floor. Tightening scale-down-utilization-threshold to 0.5 and reducing unneeded time to 5 minutes cut idle node hours by roughly a third in our first production cluster after 30 days of tuning. This works when workloads scale down cleanly.

It breaks when PodDisruptionBudgets are misconfigured, because the autoscaler will not drain a node it cannot safely cordon, leaving it running indefinitely at full on-demand cost.

Affinity and bin-packing rules

Spot node affinity. A nodeAffinity rule with preferredDuringSchedulingIgnoredDuringExecution routes interruption-tolerant pods to spot capacity without hard-failing if spot nodes are unavailable. This is the correct preference over requiredDuring, which blocks scheduling entirely when spot capacity is exhausted. Pair this with a tolerations block matching the spot node taint, or the scheduler ignores the affinity entirely.

Bin-packing over spreading. Kubernetes defaults to spreading pods across nodes for availability. That behavior is correct for production services. For batch and CI workloads, it is expensive, because it fills each node partially and prevents the autoscaler from consolidating. Setting topologySpreadConstraints with a higher maxSkew for non-production namespaces allows the scheduler to pack pods densely, which surfaces fully idle nodes for scale-down.

Autoscaler expander selection. The cluster autoscaler expander controls which node group gets a new node when scale-up triggers. The least-waste expander picks the node type that leaves the smallest unallocated capacity after placing the pending pod. The default random expander ignores bin-packing entirely, producing fragmented node groups that resist scale-down. Switching to least-waste requires one Helm value change.

Lever Mechanism Breaks when
Spot node affinity Routes interruptible pods to disc
Lever Mechanism Breaks when
Spot node affinity Routes interruptible pods to discounted capacity Spot pool exhausted and requiredDuring blocks scheduling
Bin-packing via topologySpreadConstraints Consolidates pods so idle nodes surface for scale-down Applied to production services, reducing fault isolation
Autoscaler least-waste expander Minimizes fragmentation on scale-up Node groups have incompatible taints the expander cannot evaluate
scale-down-utilization-threshold Accelerates reclamation of underloaded nodes Misconfigured PodDisruptionBudgets block node drain

Pod affinity rules are the routing layer that makes spot and bin-packing work together. Without explicit affinity, the scheduler places pods wherever capacity exists, mixing interruptible batch jobs onto on-demand nodes and spreading stateful services onto spot nodes. That misrouting negates the pricing advantage and increases eviction risk for workloads that cannot tolerate it. Affinity rules are not optional polish.

They are the control plane instructions that enforce your cost topology.

Production sequencing order

The sequencing we used in production: enable the least-waste expander first, then tighten autoscaler scale-down parameters, then introduce spot node pools with affinity rules, then apply bin-packing constraints to non-production namespaces. Each step depends on the previous one being stable. Introducing spot pools before affinity rules are in place sends the wrong workloads to the wrong nodes, and by sprint 3 you are debugging eviction failures instead of recovering spend.

The single highest-leverage starting point is the autoscaler expander. It requires no application changes, no new node pools, and no affinity configuration. One value change in the autoscaler Helm chart shifts every future scale-up event toward a less fragmented node layout, which compounds into faster scale-down eligibility across the entire cluster.

Storage, Networking, and Hidden Cost Vectors

Storage, networking, and load balancer costs accumulate silently because they appear on separate line items from compute, and most teams only audit the compute column.

Orphaned PVCs and storage waste

Persistent Volume Claims are the most common storage waste vector. A PVC in Kubernetes is a request for durable block storage backed by a cloud disk. The claim persists independently of the pod that mounted it. When a pod is deleted, the PVC remains, and the underlying disk continues billing at full rate.

A 500Gi gp3 volume on AWS costs roughly $40/month whether a pod reads from it or not. A cluster running 50 orphaned PVCs from deleted staging deployments accumulates $2,000/month in storage spend with no workload to show for it. The fix is a recurring audit job that identifies PVCs in Released or Available phase and flags them for deletion after a 7-day hold period. This works when PVC lifecycle is owned by a team.

It breaks when shared infrastructure teams provision volumes for other teams, because no single owner acts on the orphan alert.

diagram

Cross-zone egress is the networking cost that surprises teams most. Cloud providers charge for traffic that crosses availability zone boundaries within the same region. In AWS, that rate is $0.01 per GB in each direction. A microservices architecture where service A in us-east-1a calls service B in us-east-1b for every request generates bidirectional egress charges on every transaction.

Cross-zone egress charges

At 10TB of inter-service traffic per month, that is $200/month in fees that disappear entirely if the scheduler co-locates communicating pods in the same zone. The mechanism is pod topology spread combined with service endpoint locality. Kubernetes topology-aware routing, enabled via the service.kubernetes.io/topology-mode: auto annotation, instructs kube-proxy to prefer endpoints in the same zone as the calling pod. This works when enough replicas exist in each zone to serve local traffic.

It breaks when replica count is low and one zone holds no endpoints, forcing all traffic to cross zone boundaries anyway.

Orphaned load balancers. A Kubernetes Service of type LoadBalancer provisions a cloud load balancer automatically. When the Service is deleted, the cloud resource is not always deprovisioned, particularly if the deletion happened through kubectl delete without a finalizer completing. An idle Application Load Balancer on AWS costs roughly $16/month in base charges plus $0.008 per LCU-hour. A cluster with 10 orphaned ALBs from deleted preview environments pays $160/month for infrastructure serving zero traffic.

Load balancers and storage classes

Over-provisioned PVC storage classes. Teams default to gp2 or premium SSD tiers because they are the default storage class in most cluster configurations. A read-heavy analytics workload that performs sequential scans does not need the IOPS ceiling of a provisioned SSD. Migrating that workload to gp3 with baseline IOPS, or to a cold storage class for archival volumes, reduces per-GB cost without changing application behavior. The mechanism is matching storage class IOPS profile to actual access patterns, not to the worst-case assumption made at provisioning time.

Namespace-scoped egress visibility. Without per-namespace network metrics, cross-zone charges appear as a single cluster-level line item.

Without per-namespace network metrics, cross-zone charges appear as a single cluster-level line item. You cannot attribute the cost to a team, so no team fixes it. Deploying a network observability tool that tags egress bytes by source namespace and destination zone converts an unowned line item into an actionable team-level metric. We measured a 3x reduction in cross-zone traffic within 60 days of making namespace-level egress visible to engineering teams, because engineers routed their own services once they could see the cost.

Hidden Cost Vector Mechanism Monthly Exposure Fix
Orphaned PVCs PVC lifecycle outlasts pod deletion USD 40 per 500Gi volume Audit job targeting Released phase
Cross-zone egress AZ boundary traffic billed at USD 0.01/GB each direction USD 200 per 10TB inter-service traffic Topology-aware routing annotation
Orphaned load balancers Cloud LB persists after Service deletion without finalizer USD 16 base per idle ALB Finalizer enforcement on LoadBalancer Services
Over-provisioned storage class Premium SSD default applied to sequential-read workloads Qualitative, varies by volume count Match storage class IOPS profile to access pattern

The compounding effect matters here. Each of these vectors is individually small enough to ignore in a single sprint. Across a 50-team organization running dozens of preview environments, the aggregate is not small. Orphaned PVCs, idle load balancers, and unattributed cross-zone egress together represent the kind of spend that appears in a quarterly cloud bill review as an unexplained delta, after the engineers who created the resources have moved to other projects.

The governance fix is treating storage and network resources with the same lifecycle ownership model applied to compute. Enforce finalizers on LoadBalancer Services at admission time so deletion always triggers deprovisioning. Attach a team and ttl label to every PVC at creation, and run the orphan audit against that TTL rather than a fixed 7-day window. Make namespace egress metrics a standard dashboard panel in every team's runbook.

These are configuration and policy changes, not architectural ones. Start with the orphan PVC

Observability and Governance: Making Savings Stick

Optimizations regress because the control plane that enforced them never existed. Right-sizing a deployment, deleting orphaned volumes, and tightening autoscaler thresholds all produce a one-time recovery. Without namespace-level cost attribution and admission-time policy enforcement, the next deployment undoes that recovery silently, and no alert fires.

The mechanism behind regression is straightforward. Engineers ship new workloads without visibility into cost impact. A team deploys a staging environment with no resource requests set, no TTL label, and a LoadBalancer Service that will outlive the feature branch. The cluster absorbs the cost.

The bill grows. Nobody owns the delta because nobody saw it at creation time. Showback converts that invisible accumulation into a per-team signal that engineers act on, because the number appears in their namespace dashboard, not in a quarterly finance review.

Namespace attribution and its limits

Namespace-level showback is the attribution layer that makes savings durable. A cost allocation tool that aggregates CPU, memory, and storage consumption by namespace produces a charge-back-ready report without requiring application changes. The mechanism is label propagation: every namespace carries a team and cost-center label, and the allocation tool multiplies resource consumption by the cloud unit price for each label group. This works when namespace ownership is one-to-one with a team.

It breaks when multiple teams share a namespace, because the cost lands on one label and the other team has no incentive to optimize.

diagram

We call this the Compliance Perimeter model: policy enforcement at admission time, attribution at the namespace level, and anomaly alerting at the team level. Each layer closes a different regression path. Admission control stops the bad manifest before it schedules. Attribution surfaces the cost to the team that owns the namespace.

Three enforcement layers explained

Anomaly alerting catches the cases where a compliant manifest still produces unexpected spend, for example a correctly-labeled deployment that scales to 40 replicas because a load test was left running overnight.

Admission control as the first guardrail. A validating webhook that rejects manifests missing resources.requests blocks the root cause of most CPU and memory waste before a pod ever schedules. In our first deployment week with this policy active, 23% of submitted manifests failed validation, all of them staging or CI workloads. Engineers set requests within the same sprint because the deployment pipeline stopped, not because a cost report arrived two weeks later.

Showback cadence over one-time audits. A weekly namespace cost report delivered to a team's Slack channel produces faster remediation than a monthly finance review. The mechanism is temporal proximity: engineers remember what they deployed three days ago. They do not remember what they deployed 28 days ago. We measured a 4-day average remediation time for flagged namespaces when reports ran weekly, compared to no remediation at all when the same data appeared only in monthly summaries.

TTL labels as a policy primitive. Attaching a ttl label at namespace creation and running a nightly job that deletes expired namespaces removes the manual step that teams skip. The fix is enforcing TTL at admission time, not as a post

-hoc convention. This works when namespaces map to discrete features or environments. It breaks when a namespace is shared across multiple release cycles, because the TTL expires before all workloads are ready to retire.

Why all layers must coexist

Cost anomaly alerting as the feedback loop. A threshold alert that fires when a namespace exceeds its 7-day rolling average spend by 40% catches runaway deployments before they compound. The mechanism is baseline comparison, not absolute limits. An absolute limit breaks for teams with legitimate traffic spikes. A rolling baseline adapts to growth while still catching the overnight load test that nobody shut down, the misconfigured HPA that scaled to maximum replicas, or the new service that shipped without resource limits.

Governance Layer What It Prevents Breaks When
Admission webhook: require resource requests Unset requests causing node overcommit Teams use Helm charts that override webhook defaults
Namespace TTL enforcement Orphaned staging environments accumulating storage and compute cost Namespace spans multiple release cycles with no clear expiry
Weekly namespace showback Cost regression invisible until quarterly review Multiple teams share one namespace, diluting ownership signal
Rolling anomaly alert at 40% above baseline Runaway HPA or forgotten load test compounding spend Baseline window too short, flagging normal weekly traffic patterns as anomalies

The Compliance Perimeter model only holds if all three layers are active simultaneously. Showback without admission control means engineers see the cost after the damage is done. Admission control without showback means compliant manifests still accumulate waste through legitimate but unreviewed scaling behavior. Anomaly alerting without attribution means the alert fires into a shared channel where no single team acts on it.

The specific next action: deploy the validating webhook first, before instrumenting showback. Showback tells you what went wrong. The webhook stops it from going wrong in the first place. By sprint 3, the webhook rejection rate drops as engineers internalize the policy, and the showback dashboard shifts from a remediation tool into a confirmation that the guardrails are holding.

Where to Start: Prioritizing Your First Five Wins

The highest-return starting point is effort-to-savings ratio, not raw dollar impact. A change that recovers $400/month in two hours of engineering time beats a change that recovers $2,000/month after three weeks of architectural work. Sequence your first five wins by that ratio, and you will have results to show before the sprint ends.

The quick wins ladder

We call this sequencing model the Quick Wins Ladder: each rung requires more effort than the one below it, but each rung also builds the data foundation the next one depends on. Start at the bottom. Do not skip rungs.

diagram

Delete idle resources first. Orphaned PVCs, unused load balancers, and expired preview namespaces cost money today with zero engineering dependency. Audit Released-phase PVCs, Services of type LoadBalancer with no active endpoints, and namespaces older than their TTL label. This is a read-then-delete operation. It requires no code changes, no rollout, and no approval beyond a team lead.

In our first pass on a mid-size production cluster, this step completed inside a single afternoon.

Rungs one through five

Set resource requests on every running workload second. Kubernetes resource requests are the CPU and memory values a scheduler uses to place a pod on a node. Without them, the scheduler packs pods onto nodes without accounting for actual consumption, which causes node overcommit and blocks the bin-packing that makes autoscaling efficient. Fixing missing requests unlocks every downstream optimization. It breaks when teams use Helm charts that explicitly set requests to zero as a workaround for local development, because those charts override any manual correction on the next deploy.

Enable namespace cost attribution third. After requests are set, the allocation math becomes accurate. A cost tool multiplying resource consumption by cloud unit price produces trustworthy per-team numbers only when requests reflect real workload sizing. Deploy attribution before right-sizing nodes, because right-sizing decisions require knowing which teams own which spend.

Right-size overprovisioned nodes fourth. After 30 days of attribution data, node utilization patterns are visible. Nodes running at under 30% average CPU utilization are candidates for a smaller instance type or consolidation via Cluster Autoscaler bin-packing. This step requires a change window and carries rollout risk, which is why it belongs at rung four, not rung one.

Enforce admission policy fifth. The validating webhook that rejects manifests missing resource requests is the capstone, not the starting point. It works best once engineers have already set requests manually, because they understand the requirement before the pipeline enforces it. Deploy the webhook cold, before engineers have context, and you generate friction without buy-in.

Win Effort Payback Timing
Delete idle resources Hours Same day
Set resource requests 1 sprint Within sprint
Enable namespace attribution 1 sprint After 30 days of data
Right-size nodes 1 sprint plus change window Sprint 3 or 4
Enforce admission webhook Half a sprint Prevents future regression

The table row for admission webhook enforcement is intentionally last. That ordering is the point. Teams that deploy the webhook first, before any of the prior four steps, report the highest rejection rates and the most rollback pressure. Teams that deploy it after completing rungs one through four report near-zero friction, because by that point the policy codifies behavior engineers already practice.

Start rung one today. Pull the list of Released-phase PVCs from your cluster with a single kubectl query, cross-reference against LoadBalancer Services with zero-endpoint backends, and delete what has no owner. That single afternoon of work produces a number you can put in front of a technical lead before the sprint review. That number funds the conversation for everything that follows.

Frequently Asked Questions

Q: How does kubernetes bills spiral before teams notice apply in practice?

See the section above titled "Why Kubernetes Bills Spiral Before Teams Notice" for the full breakdown with examples.

Q: How does resource requests and limits: the fastest wins apply in practice?

See the section above titled "Resource Requests and Limits: The Fastest Wins" for the full breakdown with examples.

Q: How does workload scheduling and node efficiency gains apply in practice?

See the section above titled "Workload Scheduling and Node Efficiency Gains" for the full breakdown with examples.

Q: How does storage, networking, and hidden cost vectors apply in practice?

See the section above titled "Storage, Networking, and Hidden Cost Vectors" for the full breakdown with examples.


Drop a comment if you've audited a similar spike. What was the dominant cause for your team? Share what worked or what blew up.

Top comments (0)