<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samarth</title>
    <description>The latest articles on DEV Community by Samarth (@samarth_05).</description>
    <link>https://dev.to/samarth_05</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4006001%2Ff66e325c-1e90-4936-97bb-1f038565a9ea.png</url>
      <title>DEV Community: Samarth</title>
      <link>https://dev.to/samarth_05</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samarth_05"/>
    <language>en</language>
    <item>
      <title>From Spikes to Savings: Practical K8s Cost Optimization for 2026</title>
      <dc:creator>Samarth</dc:creator>
      <pubDate>Fri, 03 Jul 2026 04:33:17 +0000</pubDate>
      <link>https://dev.to/samarth_05/from-spikes-to-savings-practical-k8s-cost-optimization-for-2026-75k</link>
      <guid>https://dev.to/samarth_05/from-spikes-to-savings-practical-k8s-cost-optimization-for-2026-75k</guid>
      <description>&lt;h2&gt;
  
  
  From Spikes to Savings: Practical K8s Cost Optimization for 2026
&lt;/h2&gt;

&lt;p&gt;It started with a Slack message that every platform team dreads.&lt;/p&gt;

&lt;p&gt;"Hey, why did our AWS bill jump 34% this quarter and nothing in the product roadmap explains it?"&lt;/p&gt;

&lt;p&gt;That question landed on my desk on a Monday morning, and by Friday I had a spreadsheet full of numbers that made me slightly sick to my stomach. Our Kubernetes clusters — the ones running our checkout service, our notification pipeline, and a dozen internal tools — were burning through compute like a bonfire, and almost nobody sitting in a meeting could tell me why.&lt;/p&gt;

&lt;p&gt;This article is the story of how we found that waste, fixed it, and built a repeatable system so it never crept back in. If you're a student trying to understand what "resource requests" even mean, or a senior SRE looking for a sanity check on your VPA rollout strategy, there's something here for you. We'll start from the absolute basics and build up to production-grade Kubernetes cost optimization practices you can apply this week.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Bill That Started Everything&lt;/li&gt;
&lt;li&gt;Kubernetes 101 (For Readers Who Are New Here)&lt;/li&gt;
&lt;li&gt;The Real Villain: Requests and Limits&lt;/li&gt;
&lt;li&gt;CPU Throttling — The Silent Performance Killer&lt;/li&gt;
&lt;li&gt;Memory Waste and the OOMKill Trap&lt;/li&gt;
&lt;li&gt;Seeing the Problem: Monitoring with Prometheus and Grafana&lt;/li&gt;
&lt;li&gt;Right-Sizing, Step by Step&lt;/li&gt;
&lt;li&gt;Autoscaling: HPA vs VPA vs Cluster Autoscaler&lt;/li&gt;
&lt;li&gt;The FinOps Layer: Turning Metrics Into Money&lt;/li&gt;
&lt;li&gt;Our Results: Before and After&lt;/li&gt;
&lt;li&gt;Common Mistakes We Made (So You Don't Have To)&lt;/li&gt;
&lt;li&gt;Best Practices Checklist&lt;/li&gt;
&lt;li&gt;Final Thoughts&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. The Bill That Started Everything
&lt;/h2&gt;

&lt;p&gt;Before I explain what we did, let me set the scene. We run a mid-sized e-commerce platform. Our Kubernetes footprint was around 40 nodes across three clusters — production, staging, and a shared internal tools cluster. Nothing exotic. Standard EKS setup, standard Helm charts, standard "we'll optimize it later" engineering culture.&lt;/p&gt;

&lt;p&gt;The problem is that "later" rarely arrives on its own. Engineers ship a new microservice, guess at how much CPU and memory it needs, round up "just to be safe," and move on to the next sprint. Multiply that by 60 microservices over two years, and you get a cluster that's technically healthy but financially bloated.&lt;/p&gt;

&lt;p&gt;Our infrastructure wasn't broken. It was just wasteful — quietly, invisibly wasteful, in a way that never shows up as an incident but absolutely shows up on an invoice.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Kubernetes 101 (For Readers Who Are New Here)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gnzgfufhfgdq7k21aj3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gnzgfufhfgdq7k21aj3.png" alt="Diagram showing how Kubernetes pods are scheduled onto multiple worker nodes within a cluster" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you already know what a pod, node, and container are, skip ahead to Section 3. If not, stick with me — this matters for everything that follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt; is a system that runs and manages containerized applications across a group of machines. Think of it as an operating system for your data center or cloud account, except instead of managing files and processes on one computer, it manages containers across many computers.&lt;/p&gt;

&lt;p&gt;A few core terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt;: A packaged application with everything it needs to run (code, libraries, dependencies) bundled together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod&lt;/strong&gt;: The smallest deployable unit in Kubernetes. A pod usually wraps one container (sometimes a few tightly coupled ones).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node&lt;/strong&gt;: A physical or virtual machine that actually runs your pods. Your cluster is made up of multiple nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt;: The whole collection of nodes, managed together by Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the part that matters for cost: &lt;strong&gt;when you deploy a pod, you tell Kubernetes how much CPU and memory it needs.&lt;/strong&gt; Kubernetes uses that information to decide which node has room for it. Get that number wrong — and almost everyone gets it wrong at first — and you either starve your application or pay for capacity you never use.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Real Villain: Requests and Limits
&lt;/h2&gt;

&lt;p&gt;This is the concept that, once it clicks, changes how you think about Kubernetes forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhngi8vqdyph4rubwxz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhngi8vqdyph4rubwxz7.png" alt="Analogy graphic showing a restaurant kitchen with cooks assigned to specific stations representing CPU and memory resource requests" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Every container in a pod can define two numbers for CPU and memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requests&lt;/strong&gt;: The amount of CPU/memory the container is guaranteed to get. Kubernetes uses this number to decide which node to place the pod on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits&lt;/strong&gt;: The maximum amount the container is allowed to use. If it tries to use more, Kubernetes throttles it (for CPU) or kills it (for memory).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an analogy that finally made this click for a junior engineer on our team.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Think of a restaurant kitchen during dinner rush.&lt;/strong&gt; Every line cook gets assigned a station — a fixed amount of counter space, a burner, a cutting board. That's their &lt;strong&gt;request&lt;/strong&gt;: guaranteed space, reserved whether they're chopping vegetables or standing idle. Now imagine the head chef also sets a hard rule: "You can borrow a second burner if it's free, but never more than two burners total, no matter how busy it gets." That's the &lt;strong&gt;limit&lt;/strong&gt; — the ceiling you're never allowed to cross, even during a rush.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you request five burners per cook "just in case," but most cooks only ever use one, you've just rented a kitchen four times bigger than you need. That's exactly what was happening in our cluster. Teams had requested CPU and memory as if every service would hit peak load simultaneously, forever. In reality, most services idled at 10-15% utilization.&lt;/p&gt;

&lt;p&gt;Here's what an over-provisioned pod spec looked like in our repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;our-registry/order-service:2.4.1&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4Gi"&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we pulled actual usage data from Prometheus, this container averaged &lt;strong&gt;220m CPU&lt;/strong&gt; and &lt;strong&gt;650Mi memory&lt;/strong&gt;. We were reserving nearly 10x the CPU it actually used. Multiply that gap across 60 services, and the "phantom capacity" adds up to real nodes — nodes we were paying for every hour of every day, running practically empty.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Requests drive your bill. Limits drive your stability. Most teams tune limits carefully (to avoid crashes) and completely ignore requests (which is where the money actually leaks).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. CPU Throttling — The Silent Performance Killer
&lt;/h2&gt;

&lt;p&gt;Here's a twist that surprises a lot of engineers: &lt;strong&gt;over-provisioning and performance problems can coexist in the same cluster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkppzjzjsmqt3asiiaccm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkppzjzjsmqt3asiiaccm.png" alt="Graph illustrating how CPU throttling occurs when a container hits its defined limit, causing processing delays even when the node has available capacity" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CPU throttling happens when a container hits its CPU limit and Kubernetes forcibly slows it down, even if the node has spare CPU sitting right next to it. Kubernetes enforces limits using a mechanism called the &lt;strong&gt;Completely Fair Scheduler (CFS) quota&lt;/strong&gt;, which divides CPU time into fixed time slices. If your container burns through its slice early, it has to wait for the next one — even if seven other cores on that node are doing nothing.&lt;/p&gt;

&lt;p&gt;We found this the hard way. Our checkout service had generous memory requests but a tight CPU limit (a leftover from an old default). During flash sales, response times would spike even though our dashboards showed "plenty of CPU available" at the node level. The node had capacity. Our pod just wasn't allowed to use it.&lt;/p&gt;

&lt;p&gt;We diagnosed this using the &lt;code&gt;container_cpu_cfs_throttled_periods_total&lt;/code&gt; metric in Prometheus, graphed against &lt;code&gt;container_cpu_cfs_periods_total&lt;/code&gt; in Grafana. When the throttled-to-total ratio climbs above roughly 10-15%, your application is being strangled by its own limit, not by the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters for cost:&lt;/strong&gt; teams often respond to throttling by throwing more CPU limit at the problem — sometimes doubling or tripling it "to be safe." That fixes the symptom but re-inflates the bill. The real fix is right-sizing the limit based on actual burst behavior, not fear.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Memory Waste and the OOMKill Trap
&lt;/h2&gt;

&lt;p&gt;Memory works differently from CPU, and mixing up the two is a classic beginner mistake.&lt;/p&gt;

&lt;p&gt;CPU is compressible — Kubernetes can throttle it. Memory is not. If a container tries to use more memory than its limit allows, the Linux kernel's OOM (Out Of Memory) killer terminates the process immediately. No warning, no grace period. Your pod just dies and restarts.&lt;/p&gt;

&lt;p&gt;This asymmetry pushes engineers toward padding memory limits generously, which is usually the right instinct for reliability — but dangerous for cost if the &lt;em&gt;requests&lt;/em&gt; get padded along with the limits. Remember: requests are what Kubernetes uses for scheduling and billing-relevant capacity planning. A service that requests 8Gi but uses 1.5Gi is reserving space on a node that could otherwise host two or three additional pods.&lt;/p&gt;

&lt;p&gt;We found one internal reporting service requesting 16Gi of memory because, a year earlier, it had briefly needed that much during a one-time data migration. Nobody ever revisited the number. That single pod was single-handedly preventing its node from being downsized.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common Mistake:&lt;/strong&gt; Setting requests equal to limits "just to avoid surprises." This guarantees you're always paying for peak capacity, all day, every day, even during the 90% of the time your service is idle.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Seeing the Problem: Monitoring with Prometheus and Grafana
&lt;/h2&gt;

&lt;p&gt;You cannot right-size what you cannot measure. Before touching a single YAML file, we invested a week in visibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb07nhesgzmpddt9sw8qj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb07nhesgzmpddt9sw8qj.png" alt="Screenshot of a Grafana dashboard displaying a table of Kubernetes services with columns for CPU requested, CPU used, and percentage of utilization" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; scraping metrics from &lt;code&gt;kube-state-metrics&lt;/code&gt; and &lt;code&gt;cAdvisor&lt;/code&gt; (built into the kubelet) every 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; dashboards built on top, showing requested vs. actual usage per namespace, per deployment, and per node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most useful dashboard we built was a simple table with four columns per service:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;CPU Requested&lt;/th&gt;
&lt;th&gt;CPU Used (p95)&lt;/th&gt;
&lt;th&gt;Utilization %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;order-service&lt;/td&gt;
&lt;td&gt;2000m&lt;/td&gt;
&lt;td&gt;240m&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notification-worker&lt;/td&gt;
&lt;td&gt;1000m&lt;/td&gt;
&lt;td&gt;890m&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;internal-reports&lt;/td&gt;
&lt;td&gt;4000m&lt;/td&gt;
&lt;td&gt;310m&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sorting this table by "Utilization %" ascending instantly surfaced our worst offenders. Anything under 20% utilization went straight to the top of our right-sizing backlog.&lt;/p&gt;

&lt;p&gt;For memory, we built a parallel view using &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; compared against configured requests. This metric matters more than &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; because it excludes reclaimable cache — it reflects memory the kernel actually considers "in use."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Look at p95 or p99 usage over a 2-4 week window, not just averages. Averages hide traffic spikes; percentiles respect them without over-provisioning for the rare 1-in-1000 outlier.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  7. Right-Sizing, Step by Step
&lt;/h2&gt;

&lt;p&gt;With visibility in place, we built a repeatable process rather than a one-time cleanup. Here's the workflow we still use today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Collect at least 2-3 weeks of usage data.&lt;/strong&gt; Anything shorter misses weekly traffic patterns (weekend dips, Monday spikes, month-end batch jobs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Calculate the p95 usage for CPU and memory per container.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Set requests at p95 usage plus a small buffer&lt;/strong&gt; (we use 15-20%) rather than at peak observed usage. This absorbs normal variance without recreating the original over-provisioning problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Set limits based on burst behavior, not fear.&lt;/strong&gt; For CPU, we generally avoid hard limits on latency-sensitive services entirely (more on this below) or set them at 1.5-2x the request. For memory, we set limits closer to 1.3x the request, since memory overruns are fatal rather than throttled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Roll out gradually&lt;/strong&gt;, one namespace at a time, watching for increased restarts or latency regressions.&lt;/p&gt;

&lt;p&gt;Here's the same order-service pod after right-sizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;our-registry/order-service:2.4.1&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;300m"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;800Mi"&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;600m"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single change freed up enough headroom on its node to eliminate the need for one additional node in that node group entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on CPU limits specifically:&lt;/strong&gt; Many experienced platform engineers now recommend setting CPU &lt;em&gt;requests&lt;/em&gt; carefully but leaving CPU &lt;em&gt;limits&lt;/em&gt; unset (or very generous) for latency-sensitive workloads, relying on the node's overall capacity and the Kubernetes scheduler's bin-packing instead. This avoids the throttling trap from Section 4 entirely. We adopted this pattern for our checkout service with good results — fewer latency spikes, no meaningful cost increase, because the request (which drives cost and scheduling) stayed tight.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8. Autoscaling: HPA vs VPA vs Cluster Autoscaler
&lt;/h2&gt;

&lt;p&gt;Right-sizing gets you a good static baseline. Autoscaling handles the fact that traffic isn't static.&lt;/p&gt;

&lt;h3&gt;
  
  
  Horizontal Pod Autoscaler (HPA)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Horizontal Pod Autoscaler&lt;/strong&gt; adds or removes pod replicas based on observed metrics (usually CPU or memory utilization, but it can also use custom metrics like request queue length). It answers the question: &lt;em&gt;"Do we need more copies of this service running right now?"&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service-hpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Resource&lt;/span&gt;
      &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
          &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;65&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vertical Pod Autoscaler (VPA)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Vertical Pod Autoscaler&lt;/strong&gt; adjusts the CPU and memory &lt;em&gt;requests&lt;/em&gt; of individual pods automatically, based on historical usage. It answers a different question: &lt;em&gt;"Is each individual pod sized correctly?"&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerticalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service-vpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service&lt;/span&gt;
  &lt;span class="na"&gt;updatePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;updateMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Auto"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We started VPA in &lt;code&gt;"Off"&lt;/code&gt; mode (recommendation-only) for several weeks to sanity-check its suggestions against our own p95 calculations before ever letting it apply changes automatically. This built trust with the team and caught a few edge cases where VPA's recommendations were skewed by an unusual traffic day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster Autoscaler
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Cluster Autoscaler&lt;/strong&gt; operates one level higher — it adds or removes entire &lt;em&gt;nodes&lt;/em&gt; based on whether pods are pending (unschedulable) or nodes are sitting mostly empty. This is where right-sizing pays its biggest dividend: tighter pod requests mean the Cluster Autoscaler can pack pods more densely, which means it needs fewer nodes to run the same workload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Adjusts&lt;/th&gt;
&lt;th&gt;Question It Answers&lt;/th&gt;
&lt;th&gt;Cost Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;Number of pod replicas&lt;/td&gt;
&lt;td&gt;Do we need more copies right now?&lt;/td&gt;
&lt;td&gt;Matches capacity to traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPA&lt;/td&gt;
&lt;td&gt;CPU/memory per pod&lt;/td&gt;
&lt;td&gt;Is each pod sized correctly?&lt;/td&gt;
&lt;td&gt;Eliminates per-pod waste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster Autoscaler&lt;/td&gt;
&lt;td&gt;Number of nodes&lt;/td&gt;
&lt;td&gt;Do we need more/fewer machines?&lt;/td&gt;
&lt;td&gt;Directly reduces infrastructure spend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6z2rxpd1tmotf5r8kydh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6z2rxpd1tmotf5r8kydh.png" alt="Flowchart comparing Horizontal Pod Autoscaler, Vertical Pod Autoscaler, and Cluster Autoscaler functionalities for managing cluster resources" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common Mistake:&lt;/strong&gt; Running HPA and VPA on the same metric (CPU) for the same workload without care. They can fight each other — VPA shrinking a pod's request while HPA is simultaneously trying to add replicas based on that same shrinking number. We avoid this by using VPA primarily for memory tuning and HPA primarily for CPU-driven scaling on our high-traffic services.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  9. The FinOps Layer: Turning Metrics Into Money
&lt;/h2&gt;

&lt;p&gt;Technical right-sizing is only half the story. &lt;strong&gt;FinOps&lt;/strong&gt; — the practice of bringing financial accountability to cloud spend through cross-team collaboration — is what made our savings stick instead of quietly regressing three months later.&lt;/p&gt;

&lt;p&gt;What we actually did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost allocation by namespace and label.&lt;/strong&gt; Every team's services are tagged, so their portion of the cluster bill shows up on a dashboard they can see themselves, monthly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "cost per request" metric&lt;/strong&gt; for customer-facing services, so engineering decisions get evaluated against both performance and dollar efficiency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lightweight monthly review&lt;/strong&gt;, 30 minutes, where each team looks at their utilization trend and either justifies their current sizing or commits to a right-sizing ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FinOps isn't a finance team's spreadsheet exercise bolted onto engineering after the fact. The teams that actually reduce cost sustainably are the ones where the engineers writing the YAML can see the cost impact of their own requests, in near real time, in a dashboard they already use.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Our Results: Before and After
&lt;/h2&gt;

&lt;p&gt;Numbers, because vague success stories aren't useful to anyone trying to make the case internally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcpkb5wm0jzq7sdo5t1uc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcpkb5wm0jzq7sdo5t1uc.png" alt="Comparison chart showing the reduction in node count and compute costs alongside performance improvements before and after optimization" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average node count (production)&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;-35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average CPU utilization&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;+271%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average memory utilization&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;td&gt;+177%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly compute cost&lt;/td&gt;
&lt;td&gt;$48,200&lt;/td&gt;
&lt;td&gt;$31,900&lt;/td&gt;
&lt;td&gt;-34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (checkout service)&lt;/td&gt;
&lt;td&gt;410ms&lt;/td&gt;
&lt;td&gt;265ms&lt;/td&gt;
&lt;td&gt;-35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU throttling incidents/week&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;-89%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency improvement surprised people the most. Removing over-tight CPU limits (Section 4) and letting the scheduler bin-pack more efficiently actually made things &lt;em&gt;faster&lt;/em&gt;, not just cheaper. Cost optimization and performance optimization turned out to be the same project wearing two different hats.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Common Mistakes We Made (So You Don't Have To)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setting requests equal to limits everywhere.&lt;/strong&gt; This maximizes "safety" but guarantees you pay for peak capacity permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy-pasting resource specs between services.&lt;/strong&gt; A resource spec tuned for one workload's traffic pattern is almost never correct for another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right-sizing once and never again.&lt;/strong&gt; Traffic patterns and code change. We now re-run our right-sizing review quarterly, not as a one-time cleanup project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring init containers and sidecars.&lt;/strong&gt; Service mesh proxies and logging sidecars often carried default resource requests nobody ever revisited — small individually, significant in aggregate across hundreds of pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting averages over percentiles.&lt;/strong&gt; Average CPU usage looked fine on paper while p95 usage was quietly triggering throttling during real traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling out VPA in Auto mode without observation first.&lt;/strong&gt; Let it recommend before you let it act.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Best Practices Checklist
&lt;/h2&gt;

&lt;p&gt;Use this as a working checklist for your own cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Instrument every namespace with Prometheus and build a requested-vs-actual dashboard in Grafana.&lt;/li&gt;
&lt;li&gt;[ ] Calculate p95 usage over a 2-4 week window before setting any resource spec.&lt;/li&gt;
&lt;li&gt;[ ] Set CPU requests based on p95 usage plus a 15-20% buffer.&lt;/li&gt;
&lt;li&gt;[ ] Avoid tight CPU limits on latency-sensitive services; prefer tuning the request instead.&lt;/li&gt;
&lt;li&gt;[ ] Set memory requests and limits carefully — memory overruns kill pods instantly, so build in a real buffer.&lt;/li&gt;
&lt;li&gt;[ ] Run VPA in recommendation-only mode before enabling Auto updates.&lt;/li&gt;
&lt;li&gt;[ ] Use HPA for traffic-driven scaling, VPA for per-pod sizing, and let Cluster Autoscaler do its job on top of both.&lt;/li&gt;
&lt;li&gt;[ ] Review resource specs quarterly, not once.&lt;/li&gt;
&lt;li&gt;[ ] Give every engineering team visibility into their own namespace's cost and utilization.&lt;/li&gt;
&lt;li&gt;[ ] Treat cost optimization and performance optimization as the same initiative, not competing priorities.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  13. Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Kubernetes cost optimization isn't a one-time project you finish and check off a list. It's closer to gardening than construction — you plant good defaults, you monitor growth, and you prune regularly, because usage patterns drift and defaults get copy-pasted into new services faster than anyone remembers to question them.&lt;/p&gt;

&lt;p&gt;The good news is that the tools for doing this well are mature, well-documented, and largely already sitting in your cluster: Prometheus and Grafana for visibility, VPA and HPA for automated tuning, Cluster Autoscaler for infrastructure-level efficiency, and a FinOps culture that makes cost visible to the people actually writing the resource specs.&lt;/p&gt;

&lt;p&gt;We went from a 34% unexplained cost spike to a 34% cost &lt;em&gt;reduction&lt;/em&gt;, with better latency as a side effect — not because we found some exotic optimization technique, but because we finally looked closely at the gap between what we were reserving and what we were actually using.&lt;/p&gt;

&lt;p&gt;If there's one habit worth adopting from this whole story, it's this: before you write a resource request into a YAML file, ask what data it's based on. If the honest answer is "I guessed," that's exactly where your next round of savings is hiding.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>ai</category>
      <category>performance</category>
    </item>
    <item>
      <title>From Concept to Cluster: Building a Cost-Aware Kubernetes Strategy</title>
      <dc:creator>Samarth</dc:creator>
      <pubDate>Mon, 29 Jun 2026 07:29:16 +0000</pubDate>
      <link>https://dev.to/samarth_05/from-concept-to-cluster-building-a-cost-aware-kubernetes-strategy-1ip1</link>
      <guid>https://dev.to/samarth_05/from-concept-to-cluster-building-a-cost-aware-kubernetes-strategy-1ip1</guid>
      <description>&lt;p&gt;&lt;em&gt;How our platform engineering team discovered we were paying for three times the compute we actually needed — and what we did about it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Bill That Changed Everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was a Tuesday morning when our VP of Engineering forwarded a Slack message from the CFO. Three words: "We need to talk."&lt;/p&gt;

&lt;p&gt;Our AWS bill had crossed $140,000 for the month. Six months prior, it had been $52,000. The business hadn't tripled. Our traffic hadn't tripled. But somehow, our cloud spend had.&lt;/p&gt;

&lt;p&gt;That afternoon, our platform engineering team sat down to figure out why.&lt;/p&gt;

&lt;p&gt;What we found wasn't a rogue process or a billing glitch. It was something far more common — and far more preventable: we had built a Kubernetes infrastructure on assumptions, not data.&lt;/p&gt;

&lt;p&gt;This is the story of how we diagnosed it, fixed it, and built a culture of cost-awareness that's saved us over $65,000 a month without sacrificing performance, reliability, or developer experience.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;First, Let's Talk About Kubernetes *&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're new to Kubernetes, here's the 30-second version: it's a platform that runs your applications inside small, isolated environments called &lt;strong&gt;containers&lt;/strong&gt;, and it manages those containers across a cluster of machines (called &lt;strong&gt;nodes&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Think of Kubernetes like a very smart hotel manager. Your applications are guests. Containers are rooms. Nodes are hotel floors. Kubernetes decides which guest goes in which room, on which floor, and makes sure nobody runs out of space.&lt;/p&gt;

&lt;p&gt;Now here's where cost comes in.&lt;/p&gt;

&lt;p&gt;When you deploy an application in Kubernetes, you don't just say "run this." You also tell Kubernetes how many CPU cycles and how much memory your application needs. These declarations are called &lt;strong&gt;resource requests and limits&lt;/strong&gt; — and they are the single most important factor in how much your cluster costs.&lt;/p&gt;

&lt;h1&gt;
  
  
  Example: A basic resource definition for a Pod
&lt;/h1&gt;

&lt;p&gt;resources:&lt;br&gt;
  requests:&lt;br&gt;
    cpu: "500m"      # 500 millicores = half a CPU core&lt;br&gt;
    memory: "256Mi"  # 256 Megabytes of RAM&lt;br&gt;
  limits:&lt;br&gt;
    cpu: "1000m"     # Max 1 full CPU core&lt;br&gt;
    memory: "512Mi"  # Max 512 Megabytes of RAM&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests&lt;/strong&gt; tell Kubernetes: "Reserve this much for me." &lt;strong&gt;Limits&lt;/strong&gt; say: "Don't let me use more than this."&lt;/p&gt;

&lt;p&gt;Here's the key insight we missed for too long:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Resource requests are like reserving seats in a movie theater. Even if nobody sits in them, those seats remain unavailable for others — and you're still paying for them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;The Root Problem: We Were Guessing&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq92wo0td9q2w8i6kiaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq92wo0td9q2w8i6kiaj.png" alt="A bar chart comparison showing a " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When our services were first deployed to Kubernetes, engineers set resource requests based on gut feeling and copy-paste. A common pattern we saw:&lt;/p&gt;
&lt;h1&gt;
  
  
  What we found across 40+ deployments
&lt;/h1&gt;

&lt;p&gt;resources:&lt;br&gt;
  requests:&lt;br&gt;
    cpu: "1000m"&lt;br&gt;
    memory: "1Gi"&lt;br&gt;
  limits:&lt;br&gt;
    cpu: "2000m"&lt;br&gt;
    memory: "2Gi"&lt;/p&gt;

&lt;p&gt;These numbers looked reasonable. But when we actually looked at what our services were consuming, the story was very different.&lt;/p&gt;

&lt;p&gt;Our API gateway — which handled most of our traffic — was consistently using &lt;strong&gt;80m CPU and 120Mi memory&lt;/strong&gt; at peak. We had reserved 1,000m CPU and 1Gi memory for it.&lt;/p&gt;

&lt;p&gt;That's a &lt;strong&gt;12x over-allocation on CPU&lt;/strong&gt; and an &lt;strong&gt;8x over-allocation on memory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Multiply that across 40+ services and several environments (dev, staging, production), and you have a recipe for a bill that makes your CFO cry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding the Cost Chain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we get into the fix, it's worth understanding exactly how resource waste translates to cloud spend. This flow is crucial:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65q80jbyvm13if5gfsy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65q80jbyvm13if5gfsy6.png" alt="An infographic flow chart depicting the " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Cluster Autoscaler&lt;/strong&gt; is the mechanism that automatically adds or removes nodes from your cluster based on how much has been requested. It doesn't care whether your application is actually using those resources. It only looks at what's been &lt;em&gt;requested&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So if you have 40 services each wildly over-requesting CPU and memory, the autoscaler happily spins up more nodes to accommodate them — and your cloud provider happily invoices you for every one of those nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Build Visibility First&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You cannot fix what you cannot see. Before making a single change to resource configurations, we instrumented everything.&lt;/p&gt;

&lt;p&gt;Our monitoring stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; — Collects metrics from every pod, node, and container. Think of it as a time-series database that constantly asks every part of your cluster: "How are you doing right now?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; — Visualizes those metrics into dashboards. This is where the engineering team actually looks at the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kube-state-metrics&lt;/strong&gt; — Exports Kubernetes resource metadata (requests, limits, replica counts) as Prometheus metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most revealing dashboard we built was a &lt;strong&gt;Resource Utilization vs. Request Ratio&lt;/strong&gt; view. For each service, it plotted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requested CPU vs. Actual CPU Usage&lt;/li&gt;
&lt;li&gt;Requested Memory vs. Actual Memory Usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visual was sobering. Almost every bar in the "actual usage" column was a tiny sliver compared to the towering "requested" bars next to it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&amp;gt; &lt;strong&gt;💡 Pro Tip:&lt;/strong&gt; Deploy the Kubernetes Resource Report from the CNCF Prometheus community to get a ready-made view of resource waste across your cluster without building dashboards from scratch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The key PromQL queries that revealed our waste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CPU utilization ratio per pod
sum(rate(container_cpu_usage_seconds_total[5m])) by (pod)
  /
sum(kube_pod_container_resource_requests{resource="cpu"}) by (pod)

# Memory utilization ratio per pod
sum(container_memory_working_set_bytes) by (pod)
  /
sum(kube_pod_container_resource_requests{resource="memory"}) by (pod)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ratio below 0.3 (30%) was our threshold for "this needs attention." We found that 31 out of 40 services were below this threshold.&lt;/p&gt;

&lt;p&gt;** Step 2 — Right-Sizing: The Art of Accurate Requests**&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes right-sizing&lt;/strong&gt; means setting resource requests that actually reflect real usage — not wishful thinking, not paranoia, and not what the team set two years ago when the service was brand new.&lt;/p&gt;

&lt;p&gt;There are two approaches:&lt;/p&gt;

&lt;p&gt;** Manual Right-Sizing**&lt;/p&gt;

&lt;p&gt;Look at Prometheus data over a meaningful window (at least 2 weeks, ideally 4) and calculate the P95 (95th percentile) of actual usage. Then set your request at P95 + a small safety buffer (typically 15–20%).&lt;/p&gt;

&lt;p&gt;Why P95 and not the average? Because averages hide spikes. If your service uses 100m CPU 99% of the time but jumps to 800m during a traffic surge, setting requests at the average (say, 120m) would cause resource starvation during the surge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before right-sizing&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000m"&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi"&lt;/span&gt;

&lt;span class="c1"&gt;# After right-sizing (based on P95 + 20% buffer)&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100m"&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;150Mi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this specific service, that change reduced reserved capacity by &lt;strong&gt;10x&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;** Automated Right-Sizing with VPA**&lt;/p&gt;

&lt;p&gt;Doing this manually for 40 services is tedious and error-prone. This is where the &lt;strong&gt;Vertical Pod Autoscaler (VPA)&lt;/strong&gt; becomes your best friend.&lt;/p&gt;

&lt;p&gt;VPA is a Kubernetes component that watches your pod's actual resource usage over time and automatically recommends (or applies) more accurate resource settings.&lt;/p&gt;

&lt;p&gt;Think of VPA as a personal trainer who watches how you actually work out, then tells you: "You don't need that heavy barbell. A lighter one will do the job better."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VPA configuration in recommendation mode&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerticalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway-vpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apps/v1"&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
  &lt;span class="na"&gt;updatePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;updateMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Off"&lt;/span&gt;  &lt;span class="c1"&gt;# "Off" = recommendations only, no auto-apply&lt;/span&gt;
  &lt;span class="na"&gt;resourcePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
        &lt;span class="na"&gt;minAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50m&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;64Mi&lt;/span&gt;
        &lt;span class="na"&gt;maxAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ran VPA in &lt;code&gt;Off&lt;/code&gt; mode (recommendations only) for two weeks before trusting it with &lt;code&gt;Auto&lt;/code&gt; mode. This let us review what it was suggesting and build confidence in the numbers before applying changes automatically.&lt;/p&gt;

&lt;p&gt;VPA recommendations showed up in the object's status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VPA recommendation output (kubectl describe vpa api-gateway-vpa)&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;recommendation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerRecommendations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
      &lt;span class="na"&gt;lowerBound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;105Mi&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;85m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;140Mi&lt;/span&gt;
      &lt;span class="na"&gt;upperBound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;120m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Common Mistake:&lt;/strong&gt; Don't use VPA's &lt;code&gt;Auto&lt;/code&gt; mode on stateful workloads or services where pod restarts would cause downtime. VPA currently restarts pods to apply new resource settings — plan for this accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;**&lt;br&gt;
 Step 3 — Horizontal Scaling Done Right**&lt;/p&gt;

&lt;p&gt;Right-sizing handles over-allocation at the individual pod level. But there's another dimension: how many pods should be running at any given time?&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;Horizontal Pod Autoscaler (HPA)&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;HPA scales the &lt;em&gt;number&lt;/em&gt; of replicas of your application up or down based on observed metrics — typically CPU utilization, memory, or custom business metrics like requests-per-second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway-hpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Resource&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
        &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;  &lt;span class="c1"&gt;# Scale up when avg CPU hits 60%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before HPA, we ran 5 replicas of our API gateway 24/7. After HPA, we ran 2 replicas overnight and 4–5 during business hours. That's a 40–60% reduction in replica count during off-peak hours.&lt;/p&gt;

&lt;p&gt;The combination of right-sized requests + HPA is where you see the most dramatic cost reductions. Smaller requests mean the cluster autoscaler needs fewer nodes. Fewer replicas off-peak means even fewer nodes. The savings compound.&lt;/p&gt;

&lt;p&gt;** Step 4 — Tackling CPU Throttling**&lt;/p&gt;

&lt;p&gt;Here's a counterintuitive lesson we learned the hard way: &lt;strong&gt;low resource usage doesn't always mean everything is fine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After we right-sized our services, some of them started behaving worse — higher latency, slower response times. What happened?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU throttling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's how it works: when a container exceeds its CPU &lt;em&gt;limit&lt;/em&gt;, Kubernetes throttles it — forcibly slowing it down to stay within the cap. This doesn't appear in your CPU utilization graphs as high usage (because the process gets throttled &lt;em&gt;before&lt;/em&gt; it registers high usage). Instead, it appears as latency spikes and slow responses.&lt;/p&gt;

&lt;p&gt;The metric to watch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CPU throttling ratio per container
rate(container_cpu_cfs_throttled_seconds_total[5m])
  /
rate(container_cpu_cfs_periods_total[5m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any value above 25% is a concern. We found several services throttling 60–80% of the time after our initial right-sizing pass.&lt;/p&gt;

&lt;p&gt;The fix: separate your request tuning from your limit tuning. Keep requests tight (matching real P95 usage). Keep limits more generous (2–3x the request) to give your application headroom for brief spikes without being throttled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Final tuned configuration&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;85m"&lt;/span&gt;      &lt;span class="c1"&gt;# Tight — matches P95 usage&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;140Mi"&lt;/span&gt; &lt;span class="c1"&gt;# Tight — matches P95 usage&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250m"&lt;/span&gt;     &lt;span class="c1"&gt;# 3x request — headroom for bursts&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;350Mi"&lt;/span&gt; &lt;span class="c1"&gt;# 2.5x request — headroom for GC spikes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;** Step 5 — FinOps Practices: Making Cost Visible to Everyone**&lt;/p&gt;

&lt;p&gt;Technical fixes alone aren't enough. We learned that cost optimization is a cultural problem as much as a technical one. Engineers can't make good cost decisions if they can't see the cost impact of their choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FinOps&lt;/strong&gt; (Financial Operations) is the practice of bringing financial accountability to cloud spending — making cost data visible, actionable, and shared across engineering, finance, and business teams.&lt;/p&gt;

&lt;p&gt;The changes we made to our engineering culture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost per service tagging.&lt;/strong&gt; We added a &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt; label to every Kubernetes resource. This let us break down cloud costs by team and service using AWS Cost Explorer and Kubecost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly cost reviews.&lt;/strong&gt; We added a "cost delta" section to our weekly engineering sync. Any service whose cost increased more than 15% week-over-week got a quick review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost thresholds in CI/CD.&lt;/strong&gt; Using Infracost and OPA (Open Policy Agent), we added checks that flag pull requests introducing large resource requests without justification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example OPA policy — reject CPU requests above 2 cores&lt;/span&gt;
&lt;span class="s"&gt;package kubernetes.admission&lt;/span&gt;

&lt;span class="s"&gt;deny[msg] {&lt;/span&gt;
  &lt;span class="s"&gt;input.request.kind.kind == "Deployment"&lt;/span&gt;
  &lt;span class="s"&gt;container := input.request.object.spec.template.spec.containers[_]&lt;/span&gt;
  &lt;span class="s"&gt;cpu := container.resources.requests.cpu&lt;/span&gt;
  &lt;span class="s"&gt;to_number(trim_suffix(cpu, "m")) &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;
  &lt;span class="s"&gt;msg := sprintf("CPU request %v exceeds 2 cores — requires justification", [cpu])&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Results: Before and After&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After four months of systematic work, here's what changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly AWS Spend&lt;/td&gt;
&lt;td&gt;$140,000&lt;/td&gt;
&lt;td&gt;$74,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-47%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg CPU Utilization&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;+6.5x efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg Memory Utilization&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;td&gt;+4.4x efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node Count (prod)&lt;/td&gt;
&lt;td&gt;48 nodes&lt;/td&gt;
&lt;td&gt;22 nodes&lt;/td&gt;
&lt;td&gt;-54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P99 API Latency&lt;/td&gt;
&lt;td&gt;380ms&lt;/td&gt;
&lt;td&gt;210ms&lt;/td&gt;
&lt;td&gt;-45%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row surprised us the most. Fixing CPU throttling actually &lt;em&gt;improved&lt;/em&gt; performance. We weren't just saving money — we were making the platform faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favy30yf7a9lb0ilmmcd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favy30yf7a9lb0ilmmcd8.png" alt=": A before-and-after dashboard screenshot from Grafana, showing a dramatic drop in CPU and memory usage graphs, and a corresponding reduction in the number of active nodes, with a large, bold " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Mistakes We Made (So You Don't Have To)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Applying VPA Auto mode too early.&lt;/strong&gt; We crashed two services before we understood how VPA restarts pods. Always run in recommendation mode first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Right-sizing without monitoring throttling.&lt;/strong&gt; Cutting limits too aggressively caused latency regressions. Always check &lt;code&gt;container_cpu_cfs_throttled_seconds_total&lt;/code&gt; after changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ignoring namespace-level defaults.&lt;/strong&gt; Without LimitRange objects, new services deployed with no resource requests at all — they defaulted to unlimited, which is just as bad as over-requesting.&lt;/p&gt;

&lt;h1&gt;
  
  
  Always set LimitRange in every namespace
&lt;/h1&gt;

&lt;p&gt;apiVersion: v1&lt;br&gt;
kind: LimitRange&lt;br&gt;
metadata:&lt;br&gt;
  name: default-limits&lt;br&gt;
spec:&lt;br&gt;
  limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;default:
  cpu: "500m"
  memory: "256Mi"
defaultRequest:
  cpu: "100m"
  memory: "128Mi"
type: Container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Optimizing prod but ignoring dev/staging.&lt;/strong&gt; Our staging environment was consuming 35% of our total cluster cost. Switching staging to spot/preemptible instances and applying aggressive right-sizing there alone saved $11,000/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. One-time optimization vs. continuous practice.&lt;/strong&gt; Resources drift. New services ship with wrong configs. The optimization is never "done." Build it into your CI/CD process.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where to Start: A Practical Checklist&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you're starting from scratch, here's the order that worked for us:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1:&lt;/strong&gt; Deploy Prometheus + Grafana if not already present. Build utilization ratio dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2:&lt;/strong&gt; Identify the 5 highest-cost namespaces or services. Focus there first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3:&lt;/strong&gt; Deploy VPA in recommendation mode for those services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4:&lt;/strong&gt; Apply right-sized requests based on VPA recommendations + P95 analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 5:&lt;/strong&gt; Monitor CPU throttling metrics. Tune limits accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 6:&lt;/strong&gt; Add HPA to variable-traffic services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 7:&lt;/strong&gt; Add LimitRange defaults to all namespaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 8:&lt;/strong&gt; Deploy Kubecost or OpenCost for continuous cost visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing:&lt;/strong&gt; Weekly cost review. CI/CD resource request checks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The most important lesson from this entire journey isn't a YAML snippet or a Prometheus query. It's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes doesn't optimize itself. You have to build the culture and tooling that makes optimization the path of least resistance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukola7dmbqwwdwb7r3vg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukola7dmbqwwdwb7r3vg.png" alt="A photograph of a team of platform engineers huddled around a monitor, collaboratively reviewing a dashboard, reflecting a culture of shared responsibility and successful cost-awareness in DevOps." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When cost data is invisible, engineers make expensive decisions by default — not out of carelessness, but because they genuinely can't see the impact of their choices. When you make cost visible, accurate, and tied to real services and teams, the behavior changes on its own.&lt;/p&gt;

&lt;p&gt;We went from a $140K monthly bill to $74K without removing a single feature, degrading a single SLA, or asking any team to "do less." We just got honest about what we were actually using versus what we were paying for.&lt;/p&gt;

&lt;p&gt;That gap — between what you reserve and what you use — is where your cost savings live. Close it systematically, and the bill takes care of itself.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;em&gt;If your team is on this journey, the most valuable thing you can do right now is run that first utilization ratio query against your cluster. Whatever you find, I can almost guarantee it'll be eye-opening.&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have questions or want to share your own Kubernetes cost optimization story? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently Asked Questions (FAQs)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. What exactly is Kubernetes right-sizing and why does it matter for cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right-sizing means setting resource requests and limits that accurately reflect what your application actually consumes — not what someone guessed two years ago. When requests are too high, Kubernetes reserves more node capacity than needed. The Cluster Autoscaler adds extra nodes to satisfy those reservations, and your cloud provider bills you for every one of them — whether your app uses those resources or not. Even a 2x over-allocation across 30 services can silently double your infrastructure bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How is VPA different from HPA, and when should I use each?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Vertical Pod Autoscaler (VPA) tunes how much CPU and memory a single pod is allocated — it right-sizes the resource requests on your existing pods. The Horizontal Pod Autoscaler (HPA) changes how many replicas of your pod are running. Use VPA for services where traffic volume is relatively stable but resource configs are unknown or stale. Use HPA for services with variable traffic that needs to scale up and down. For most production services, both working together gives you the best cost-to-performance ratio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. My CPU utilization looks low, but latency is high. What's happening?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is almost certainly CPU throttling. When a container hits its CPU limit, Kubernetes enforces a hard cap using Linux CFS (Completely Fair Scheduler) quotas. The process gets paused mid-execution — so CPU usage appears low in your dashboards, but your application is literally being frozen in place during request processing. Check the container_cpu_cfs_throttled_seconds_total metric in Prometheus. A throttle ratio above 25% is a clear signal your CPU limit is too tight relative to actual burst needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. How much can teams realistically save with Kubernetes cost optimization?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Industry benchmarks consistently show that teams new to right-sizing discover 50–70% over-allocation on average. Real-world results vary by maturity: teams running untuned workloads typically achieve 30–50% cost reduction in the first optimization pass. Combining right-sizing with HPA, Cluster Autoscaler tuning, spot/preemptible nodes for non-critical workloads, and namespace-level defaults often pushes total savings to 40–60% of the original bill — without removing a single feature or degrading reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. What monitoring tools should I set up before starting Kubernetes cost optimization?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At minimum, you need Prometheus (to collect pod and node metrics), Grafana (to visualize utilization vs. requests), and kube-state-metrics (to export resource request/limit data as Prometheus metrics). The most important dashboard to build first is a utilization ratio view: actual CPU usage divided by requested CPU, per pod. Any service consistently below 30% utilization is a right-sizing candidate. For cost attribution per team or service, Kubecost or OpenCost adds a cost layer on top of your existing Prometheus data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. What's the difference between resource requests and limits in Kubernetes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;requests are the guaranteed amount Kubernetes reserves on a node for your container — this directly drives scheduling and node costs. limits are the maximum your container is allowed to consume. A container that exceeds its CPU limit gets throttled (slowed down). One that exceeds its memory limit gets killed and restarted. The key mistake teams make is setting both too high "just to be safe" — which leads to massive over-provisioning across the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Is Kubernetes cost optimization a one-time project or ongoing work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ongoing — full stop. Resource configs drift constantly: new services ship with copy-pasted (often inflated) values, traffic patterns change seasonally, new engineers don't know the optimization conventions, and applications are rewritten with different performance profiles. The teams that sustain cost efficiency treat it as a continuous practice: weekly cost reviews, resource request checks in CI/CD pipelines, VPA recommendations reviewed monthly, and LimitRange defaults enforced at the namespace level so no service can accidentally deploy without resource configs.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop guessing. Start optimizing your Kubernetes costs with real data.
&lt;/h2&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;p&gt;Managing Kubernetes costs shouldn't require constant manual intervention. As cloud-native environments grow in complexity, intelligent optimization becomes essential for improving efficiency and reducing infrastructure waste.&lt;br&gt;
&lt;a href="https://ecoscale.dev" rel="noopener noreferrer"&gt;EcoScale&lt;/a&gt; gives platform teams deep visibility into their Kubernetes environments, so every scaling decision is backed by data, not instinct.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Improve resource utilization across all workloads&lt;/li&gt;
&lt;li&gt;Cut unnecessary cloud spending without touching SLAs&lt;/li&gt;
&lt;li&gt;Make informed scaling decisions with AI-driven insights&lt;/li&gt;
&lt;li&gt;Build more efficient Kubernetes infrastructure over time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;🌐 Learn more: &lt;a href="https://ecoscale.dev" rel="noopener noreferrer"&gt;https://ecoscale.dev&lt;/a&gt;&lt;/strong&gt;/&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nss7al47b10b7hqigs3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nss7al47b10b7hqigs3.jpeg" alt="Ecoscale" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
