You’ve likely stared at your AWS or GCP bill at 2:00 AM, wondering how a cluster running a handful of microservices is costing as much as a small fleet of luxury SUVs. You look at your monitoring dashboard, and the CPU utilization is a flat line at 5%. You check your nodes, and they’re at 90% capacity. This disconnect isn’t a bug in the cloud provider’s billing engine; it’s the “Request Tax”—the silent killer of Kubernetes efficiency.
We’ve all been there. A developer, anxious about a production deployment, sets a 2-core CPU request and 4GB of RAM for a service that barely handles ten requests a second. They do it for “safety.” But in Kubernetes, that safety is an expensive illusion. The scheduler doesn’t care about what your app actually uses; it only cares about what you asked for. If you ask for 4GB, that 4GB is gone from the available pool, even if your app is only sipping 200MB. You are paying for the “ghost” of resources that will never be used.
Direct Practical Verdict:
Kubernetes right-sizing requires aligning pod resource requests with 95th-percentile actual utilization rather than peak limits. Because the scheduler allocates nodes based on requests, over-provisioning creates “ghost” reservations that trigger unnecessary cluster autoscaling. By using VPA in recommendation mode and Prometheus metrics, you can reclaim up to 50% of idle cloud spend without sacrificing stability.
Why does my Kubernetes bill keep climbing even when traffic is low?
To understand the waste, we have to look at the Kubernetes Scheduler as a high-stakes real estate agent. When you submit a Pod manifest, the Scheduler looks at the `requests` field. It doesn’t look at your historical Prometheus data or your “best intentions.” It looks at the contract. If a node has 16GB of RAM and you have four pods requesting 4GB each, that node is “full.” Even if those pods are currently idling at 100MB of actual usage, the Scheduler will refuse to place a fifth pod on that node. When that fifth pod arrives, the Cluster Autoscaler sees a “Pending” pod that can’t fit anywhere and triggers the provision of a brand-new EC2 instance or Compute Engine VM. You are now paying for a whole new server because of a lie told in a YAML file. This is the “Bin Packing” problem. Efficient Kubernetes management is about packing as many pods as possible onto the fewest number of nodes without causing performance degradation. When requests are set too high, you end up with “Swiss Cheese” nodes—nodes that are technically full but practically empty. This gap between requested resources and used resources is the primary source of enterprise cloud waste.How do CPU limits and memory limits actually affect my application?
There is a fundamental misunderstanding in the community regarding the difference between `requests` and `limits`, and more importantly, how the Linux kernel handles them. This isn’t just about cost; it’s about the life and death of your process. CPU is a “compressible” resource. If your pod hits its CPU limit, Kubernetes doesn’t kill it. Instead, the kernel uses the Completely Fair Scheduler (CFS) to throttle the process. Your app doesn’t crash; it just gets incredibly slow. Latency spikes, and your p99s go through the roof. This is why many seasoned engineers recommend setting CPU requests based on actual usage but being very generous—or even omitting—CPU limits for latency-sensitive services. Memory, however, is “incompressible.” If your pod tries to use more memory than its limit, the kernel has no choice but to invoke the OOM (Out of Memory) Killer. Your process is terminated instantly. Because of this, developers often set massive memory requests to avoid the dreaded `OOMKilled` status. But by doing so, they lock up expensive RAM that sits idle 99% of the time. The goal is to find the “Goldilocks Zone”: requests high enough to ensure the pod has what it needs to start and handle normal load, but low enough to allow the scheduler to pack nodes efficiently.| Metric Category | The “Safe” Guess (Wasteful) | The Right-Sized Reality | System Impact | FinOps Result |
|---|---|---|---|---|
| CPU Requests | 2000m (2 Cores) | 120m (p95 Usage) | Higher Node Density | 94% Cost Reduction |
| Memory Requests | 4GiB | 512MiB | Reduced OOM Risk (via VPA) | 87% Cost Reduction |
| Node Count (200 Pods) | 50 Nodes (m5.xlarge) | 12 Nodes (m5.xlarge) | Optimal Bin Packing | $7,200/mo Savings |
| Scaling Trigger | False Scarcity | Actual Demand | Predictable Performance | Stable Cloud Budget |
What PromQL queries should I use to find over-provisioned pods?
You cannot manage what you do not measure. If you are using Prometheus (and if you aren’t, that’s our first problem), you already have the data needed to fix your waste. We want to look for the delta between `kube_pod_container_resource_requests` and `container_cpu_usage_seconds_total`. Here is the “Wall of Shame” query for CPU. It identifies containers where the request is significantly higher than the actual 1-hour peak usage:
avg by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="cpu"}
) -
avg by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{container!=""}[1h])
) > 0.5
For memory, we want to look at the “Working Set Bytes,” which is what the kernel actually considers “in use” for OOM purposes. This query finds pods that are requesting at least 1GB more than they have used in the last 24 hours:
avg by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="memory"}
) -
max by (namespace, pod, container) (
container_memory_working_set_bytes{container!=""}
) > 1024 * 1024 * 1024
Running these queries will likely reveal that 80% of your services are over-provisioned by a factor of five or more. This is your roadmap for savings.
How do I set up Vertical Pod Autoscaler without crashing my pods?
The manual approach—running queries and updating YAMLs—is a losing battle. Your application code changes, your traffic patterns shift, and your right-sizing becomes obsolete in a week. You need automation. The Vertical Pod Autoscaler (VPA) is the native Kubernetes solution, but most people are afraid of it because they think it will reboot their pods constantly. Here’s the secret: Recommendation Mode. When you deploy a VPA object, you can set the `updateMode` to `Off`. In this mode, the VPA controller observes the pod’s actual usage and calculates the ideal requests, but it never actually touches the pod. It simply stores the recommendation in the VPA object. This allows you to use a tool like Goldilocks. Goldilocks is an open-source dashboard that aggregates these VPA recommendations and presents them in a clean UI. It tells your developers: “Hey, you requested 2 CPUs, but we’ve observed you only need 150m. Here is the YAML to fix it.” By putting the data in front of the people who own the services, you shift the culture from “over-provision for safety” to “right-size for efficiency.”The Actionable Playbook: The 4:00 PM Resolution
If you want to slash your cluster costs by tomorrow morning, follow this exact sequence. Don’t try to boil the ocean; start with the biggest offenders. 1. Install the VPA Recommender: Deploy the Vertical Pod Autoscaler components into your cluster. You don’t need the “Updater” or “Admission Controller” yet—just the “Recommender.” 2. Deploy Goldilocks: Install Goldilocks via Helm and label your namespaces with `goldilocks.fairwinds.com/enabled=true`. This will automatically create VPA objects in “Off” mode for every deployment in those namespaces. 3. Wait 24 Hours: Let the recommender gather data through a full business cycle. This ensures you capture peak traffic periods. 4. Identify the “Top 10” Wasters: Use the PromQL queries provided above to find the ten deployments with the largest gap between requested and used resources. 5. Implement “Limitless” CPU: For these top 10, set the CPU `requests` to the VPA’s “Target” recommendation. Remove the CPU `limits` entirely to prevent CFS throttling during startup or unusual bursts. 6. Set Conservative Memory: Set memory `requests` to the VPA’s “Target” recommendation, but set the memory `limit` to 20% above that to provide a buffer against OOM kills. 7. Monitor and Repeat: Watch your Cluster Autoscaler logs. As you update these deployments, you will see nodes becoming under-utilized. The autoscaler will begin draining and terminating unneeded EC2/GCE instances. By following this playbook, you aren’t just guessing; you are using empirical data to drive infrastructure decisions. You’re moving from a reactive “why is the bill so high” posture to a proactive FinOps culture. Kubernetes is a powerful tool for scaling, but without disciplined resource management, it’s just a very expensive way to run a Linux server. Right-size your requests, reclaim your budget, and get back to building features instead of funding cloud provider dividends.Frequently Asked Questions
What is the difference between a resource request and a resource limit? A request is what the Kubernetes scheduler uses to place a pod on a node; it is a guaranteed reservation. A limit is the maximum amount of a resource a pod is allowed to consume. If a pod exceeds its CPU limit, it is throttled; if it exceeds its memory limit, it is terminated (OOMKilled). Why should I avoid setting CPU limits for microservices? CPU limits can cause unnecessary throttling even when the node has idle CPU cycles available. This happens due to the way the Linux kernel enforces quotas over short time slices. Removing CPU limits allows pods to “burst” into idle capacity, improving latency without increasing costs. Does lowering resource requests affect application performance? If done correctly using 95th-percentile data, lowering requests should not affect performance. Requests are primarily for scheduling. As long as the node has enough physical capacity to handle bursts, the pod will perform normally. The risk only occurs if you under-provision requests so much that the node becomes physically over-saturated.
Kubernetes Resource Right-Sizing & Overprovisioning Remediation
Optimizing database memory and compute allocations mirrors container resource right-sizing strategies. For pod waste elimination, consult our report on PostgreSQL Cloud Repatriation: Bare-Metal NVMe Speed.
