AKS Cost Optimization: Resource Governance That Actually Works
AKS costs are brutally simple: node sizing, pod density, workload sprawl, and reserved capacity. If you don’t have visibility and governance, your cloud bill will punch you in the face—usually when it’s too late to react without pain. I’ve watched teams scramble to cut costs after the invoice lands, breaking production in the process. This guide is for practitioners who want to avoid that mess. No theory, no vendor fluff: just what actually works to keep AKS costs under control without sacrificing reliability.
The problem: visibility prevents shocks
The real risk is flying blind on cost. Most organizations throw infrastructure at problems, then act shocked when the bill arrives. This delay creates a vicious cycle: teams over-provision to avoid risk, costs spiral, finance panics, and engineers are told to “just cut spend”—usually with zero context. If you don’t know what your resources will cost before you deploy, you’re setting yourself up for failure. In AKS, node cost is king. If you don’t understand node sizing, pod density, and workload distribution, you’re not in control: you’re just hoping for the best.
Pod density vs. node size
Pod density: the number of pods per node. Higher density slashes per-pod cost, but if you push it too far, a single node failure can wipe out half your workloads. Lower density means more nodes, more overhead, and more Azure spend for the same business value. Most teams get this wrong by guessing or copying defaults.
Here’s the honest assessment: Small nodes with low pod density are easy to replace, but you pay for the privilege. Large nodes with high pod density are efficient, but when they go down, you feel it. For most real-world workloads, start with medium-sized nodes (4-8 vCPUs, 16-32 GB RAM) and target 20-30 pods per node. Don’t trust vendor sizing calculators: watch your actual pod usage and adjust. Memory hogs need lower density. Stateless microservices? Push the density, but monitor for pain.
Node size hits your Azure bill directly. A Standard_D4s_v5 (4 vCPU, 16 GB) is about $140/month in West Europe. A D8s_v5 (8 vCPU, 32 GB) is $280/month. If you run 40 pods at 0.5 vCPU and 2 GB RAM each, you can fit them on two D4s_v5 or one D8s_v5. The cost is identical, but the blast radius is not. Lose the big node, lose everything on it. The real cost difference? System overhead. Kubernetes always takes a cut for kubelet, container runtime, and system pods. Small nodes waste more on overhead. Big nodes are more efficient, but you pay in risk.
Node-pool stratification
Mixing workloads in a single node pool is a rookie mistake. Batch jobs, web services, databases, and background workers all have different needs. If you lump them together, you guarantee wasted money and operational headaches. Stratify your node pools. Optimize each for cost, performance, and reliability. No exceptions.
Create separate node pools for:
- System workloads: Small, reliable nodes for kube-system and monitoring
- Production services: Standard nodes for user-facing apps
- Batch/background jobs: Spot or big nodes for interruptible stuff
- Stateful services: Nodes with local SSD for low-latency storage
But here’s the catch: Node-pool stratification is useless if you don’t enforce it. Use node affinity and taints/tolerations. If you skip this, your pods will land wherever they want, and your cost model is toast.
Example configuration for a production node pool with cost labels in Terraform:
resource "azurerm_kubernetes_cluster_node_pool" "production" {
name = "production"
kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
vm_size = "Standard_D4s_v5"
node_count = 3
tags = {
environment = "production"
workload = "web-services"
cost-center = "engineering"
managed-by = "terraform"
}
node_labels = {
"workload-type" = "production"
"node-pool" = "production"
}
node_taints = [
"workload=production:NoSchedule"
]
}
resource "azurerm_kubernetes_cluster_node_pool" "batch" {
name = "batch"
kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
vm_size = "Standard_D8s_v5"
priority = "Spot"
eviction_policy = "Delete"
spot_max_price = -1 # Pay up to regular price
node_count = 1
enable_auto_scaling = true
min_count = 1
max_count = 5
tags = {
environment = "production"
workload = "batch-processing"
cost-center = "data-engineering"
managed-by = "terraform"
}
node_labels = {
"workload-type" = "batch"
"node-pool" = "batch"
"kubernetes.azure.com/scalesetpriority" = "spot"
}
node_taints = [
"workload=batch:NoSchedule",
"kubernetes.azure.com/scalesetpriority=spot:NoSchedule"
]
}
Tag your node pools with cost-center and workload. If you don’t, finance will treat your AKS bill as a black box, and you’ll lose every argument about budget. Azure Cost Management can only help if you give it the data.
Spot VMs and reserved instances
Spot VMs are the cloud’s version of a fire sale: up to 90% off, but you can get kicked out with 30 seconds notice. Use them for workloads that can be interrupted—batch jobs, CI/CD runners, dev environments, stateless background stuff. If you put production or stateful workloads on spot VMs without bulletproof redundancy, you’re asking for trouble. The 30-second eviction is real, and Azure doesn’t care about your SLAs.
Reserved instances are the opposite: predictable discounts (up to 72% for 3-year commitments), but you pay whether you use them or not. Only buy what you know you’ll need for the next 1-3 years. Overcommit, and you’re stuck.
Cost model comparison for a Standard_D4s_v5 node in West Europe:
- Pay-as-you-go: ~$140/month
- 1-year reserved: ~$100/month (29% discount)
- 3-year reserved: ~$65/month (54% discount)
- Spot VM (typical): ~$20-40/month (70-85% discount, variable)
Risk models for spot vs. reserved instances:
- Spot VMs: high discount, high operational risk, suitable for interruptible workloads
- 1-year reserved: moderate discount, low risk, suitable for proven steady-state capacity
- 3-year reserved: high discount, moderate risk (commitment lock-in), suitable for long-term stable workloads
Author tip: Spot VMs for batch, dev, and test. 1-year reserved for production baseline, but only after 3-6 months of real usage data. Avoid 3-year commitments unless you’re absolutely sure. Cloud changes fast: don’t lock yourself in.
Resource requests and limits
Resource requests and limits are where most teams burn money. Over-requesting is rampant: people set requests based on worst-case guesses, not real data. The result? Nodes look full, but are actually running at 20-30% utilization. Kubernetes blocks new pods, you scale out, and your CFO wonders why the bill keeps climbing.
Under-requesting is just as bad: pods get scheduled, but then fight for resources, get throttled, or OOMKilled. Kubernetes thinks there’s room, but your workloads suffer.
The fix is not rocket science, but it requires discipline. Monitor actual usage with Azure Monitor or Prometheus. Set requests to the 95th percentile of real usage, not what you wish was true. Example: If a pod averages 200m CPU and 500Mi memory, but spikes to 400m/1Gi, set requests to 250m/600Mi, limits to 500m/1.5Gi. Review and adjust regularly. If you don’t, you’re just guessing—and paying for it.
Incorrect resource settings create a cascading waste problem. Over-requested resources kill node packing, so you run more nodes than you need. More nodes mean more cost, more operational pain, and more complexity. Don’t be lazy: measure, set, and review. It’s the only way.
Cost attribution and chargeback
Cost attribution is not optional. If you can’t answer “who owns this cost?”, you’re going to lose every budget discussion. Finance hates black boxes. Tag your node pools, disks, and load balancers with cost-center, team, and workload. If you don’t, your AKS bill is just a giant question mark.
Practical implementation steps:
- Tag all AKS resources (node pools, disks, load balancers) with cost-center, workload, environment, and managed-by tags
- Configure Azure Cost Management to group costs by these tags
- Export cost data monthly and distribute reports to team leads
- Review high-cost workloads and investigate optimization opportunities
Kubernetes-native attribution is possible. Use Kubecost or OpenCost to estimate per-pod cost based on real resource requests and node pricing. Aggregate by namespace, label, or deployment. If you don’t have this visibility, you’re just hoping your spend is “reasonable”. See the Kubecost documentation and OpenCost project.
Example Azure CLI query for cost by tag:
# Query AKS costs grouped by cost-center tag for the last 30 days
az costmanagement query \
--type Usage \
--timeframe MonthToDate \
--scope "/subscriptions/<subscription-id>" \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-grouping name="Tags" type="Tag" \
--query 'properties.rows[]' \
--output table
# Export detailed cost breakdown to CSV
az costmanagement query \
--type Usage \
--timeframe MonthToDate \
--scope "/subscriptions/<subscription-id>" \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-grouping name="Tags" type="Tag" \
--output json | jq -r '.properties.rows[] | @csv' > aks-costs.csv
Chargeback or showback? Chargeback means teams pay for what they use. Showback just shows them the numbers. Start with showback—it’s less political. But if you want real accountability, move to chargeback once teams have seen the data and had a chance to optimize. Don’t try to force chargeback on day one unless you like chaos.
Practical checklist
Checklist for teams who want to stop wasting money:
- Measure before optimizing: 30 days of real pod usage before you touch requests/limits.
- Tag everything: Cost-center, workload, environment—no exceptions.
- Stratify node pools: Production, batch, system—separate or pay the price.
- Right-size nodes: 20-30 pods per node for most. Adjust for reality, not theory.
- Use spot VMs for batch: 70-85% discounts, but only for workloads that can die anytime.
- Reserve baseline capacity: 1-year reserved, but only after you know your steady state.
- Set accurate requests: 95th percentile of real usage, not guesses.
- Enable autoscaling: Let the cluster scale up/down based on pending pods.
- Review monthly: Export cost data, find the top spenders, and dig in.
- Automate reporting: Monthly cost breakdowns by team, sent automatically. No excuses.
Cost optimization never ends. Workload patterns shift, prices change, new VM types appear. Review every month. If you don’t, your bill will creep up and nobody will notice until it’s a crisis.
Conclusion
AKS cost optimization is not about slashing spend after the fact. It’s about ruthless visibility, honest governance, and making the right calls up front. If you don’t understand pod density, node-pool design, spot VM risk, and resource requests, you’re just hoping for a good outcome. Hope is not a strategy.
Tag everything, export cost data, and make sure the people who control resources see the numbers. Technical optimization plus organizational accountability is the only way to keep your cloud bill from spiraling out of control. Ignore this, and you’ll be back here next quarter, wondering where all the money went.

Comments