This page is generated from devops-agent/eks-cost-intelligence/references/waste-calculation.md. Edit the source, not this page.
Waste Calculation Formulas
Part of: eks-cost-intelligence Purpose: Dollar waste formulas for computing waste from utilization gaps, idle nodes, storage inefficiency, network costs, and missed Spot/Graviton opportunities
Core Principle
Waste = money spent on capacity that is allocated but not used, or money overspent by not using cheaper alternatives.
Every formula follows the same structure:
- Inputs — what data you need to collect
- Calculation — the formula to compute waste
- Output —
monthly_wasteandmonthly_savingsvalues for the finding
1. Compute Waste (Request vs P95 Utilization)
Per-Pod Compute Waste
Inputs
| Input | Source | Example |
|---|---|---|
cpu_request_cores | Pod spec .resources.requests.cpu | 0.800 (800m) |
cpu_p95_cores | metrics-server / Container Insights / Prometheus | 0.065 |
mem_request_bytes | Pod spec .resources.requests.memory | 1073741824 (1Gi) |
mem_p95_bytes | metrics-server / Container Insights / Prometheus | 398458880 (380Mi) |
pod_monthly_cost | Allocated share of node cost (see cost-estimation-fallback.md) | $185.00 |
Calculation
# Step 1: Calculate waste ratios for each resource
cpu_waste_ratio = max(0, (cpu_request_cores - cpu_p95_cores) / cpu_request_cores)
mem_waste_ratio = max(0, (mem_request_bytes - mem_p95_bytes) / mem_request_bytes)
# Step 2: Use the LOWER ratio (conservative — waste only what BOTH resources confirm)
waste_ratio = min(cpu_waste_ratio, mem_waste_ratio)
# Step 3: Calculate dollar waste
monthly_waste = waste_ratio * pod_monthly_cost
# Step 4: Calculate savings (account for 1.5x headroom buffer on right-sized requests)
headroom_factor = 0.85 # savings after keeping 15% headroom above p95
monthly_savings = monthly_waste * headroom_factor
Worked Example
Pod: payments/checkout-7f8b9c-abc12
CPU request: 800m (0.800 cores)
CPU P95 actual: 65m (0.065 cores)
Memory request: 1Gi (1024 MiB)
Memory P95 actual: 380Mi
Step 1:
cpu_waste_ratio = (0.800 - 0.065) / 0.800 = 0.919 (91.9%)
mem_waste_ratio = (1024 - 380) / 1024 = 0.629 (62.9%)
Step 2:
waste_ratio = min(0.919, 0.629) = 0.629
Step 3:
pod_monthly_cost = $185.00
monthly_waste = 0.629 × $185.00 = $116.37
Step 4:
monthly_savings = $116.37 × 0.85 = $98.91
Finding:
monthly_waste: $116.37
monthly_savings: $98.91
fix: Set CPU to 100m (1.5× P95), memory to 570Mi (1.5× P95)
Per-Node Compute Waste (Aggregate)
Inputs
| Input | Source | Example |
|---|---|---|
node_allocatable_cpu | kubectl get node -o json → .status.allocatable.cpu | 4.0 cores |
node_total_requests_cpu | Sum of all pod CPU requests on node | 1.2 cores |
node_p95_cpu | CloudWatch / Prometheus node-level metric | 0.8 cores |
node_hourly_cost | Instance pricing (see table below) | $0.192 |
Calculation
# Node-level waste: gap between total requests and actual usage
node_waste_ratio = max(0, (node_total_requests_cpu - node_p95_cpu) / node_allocatable_cpu)
node_monthly_cost = node_hourly_cost * 24 * 30
monthly_waste = node_waste_ratio * node_monthly_cost
Severity Thresholds
| Waste Ratio | Severity | Action |
|---|---|---|
| > 70% | CRITICAL | Right-size immediately |
| 40–70% | HIGH | Review and adjust within sprint |
| 20–40% | MEDIUM | Monitor trend, plan adjustment |
| < 20% | LOW | Acceptable headroom |
2. Idle Node Waste
Inputs
| Input | Source | Example |
|---|---|---|
node_avg_cpu_7d | CloudWatch CPUUtilization (7-day avg) | 6% |
node_avg_mem_7d | CloudWatch / kubelet metrics (7-day avg) | 12% |
instance_type | kubectl get node labels | m5.xlarge |
instance_hourly_rate | AWS Price List (see reference table) | $0.192 |
node_has_gpu | Node labels nvidia.com/gpu | false |
Reference: Instance Hourly Rates (us-east-1, On-Demand)
⚠️ Fallback only — last verified June 2026. Prefer the AWS Price List API for production assessments. See
cost-estimation-fallback.mdStep 2.
| Instance Type | vCPU | Memory | Hourly Rate |
|---|---|---|---|
| m5.large | 2 | 8 GiB | $0.096 |
| m5.xlarge | 4 | 16 GiB | $0.192 |
| m5.2xlarge | 8 | 32 GiB | $0.384 |
| m6i.large | 2 | 8 GiB | $0.096 |
| m6i.xlarge | 4 | 16 GiB | $0.192 |
| m6i.2xlarge | 8 | 32 GiB | $0.384 |
| m7i.xlarge | 4 | 16 GiB | $0.202 |
| m7i.2xlarge | 8 | 32 GiB | $0.403 |
| m6g.xlarge | 4 | 16 GiB | $0.154 |
| m7g.xlarge | 4 | 16 GiB | $0.163 |
| c5.xlarge | 4 | 8 GiB | $0.170 |
| c5.2xlarge | 8 | 16 GiB | $0.340 |
| c7i.xlarge | 4 | 8 GiB | $0.178 |
| c7g.xlarge | 4 | 8 GiB | $0.145 |
| r5.xlarge | 4 | 32 GiB | $0.252 |
| r5.2xlarge | 8 | 64 GiB | $0.504 |
| r7i.xlarge | 4 | 32 GiB | $0.264 |
| r7g.xlarge | 4 | 32 GiB | $0.214 |
For instance types not in this table, use the AWS Price List API:
Use the AWS Price List GetProducts API to look up hourly instance rates for types not in the reference table.
Detection Criteria
A node is idle if ALL of:
- Average CPU utilization < 10% over 7 days
- Average memory utilization < 20% over 7 days
- No GPU workloads scheduled (
nvidia.com/gpunot present)
Calculation
# Per idle node
is_idle = (node_avg_cpu_7d < 0.10 and
node_avg_mem_7d < 0.20 and
not node_has_gpu)
if is_idle:
monthly_waste = instance_hourly_rate * 24 * 30
monthly_savings = monthly_waste # full node cost is recoverable
# Aggregate across all idle nodes
total_idle_waste = sum(node.monthly_waste for node in idle_nodes)
Worked Example
Node: ip-10-0-1-42.ec2.internal
Instance type: m5.xlarge
Avg CPU (7d): 6%
Avg Memory (7d): 12%
GPU workloads: none
Detection:
6% < 10% ✓ AND 12% < 20% ✓ AND no GPU ✓ → IDLE
Calculation:
monthly_waste = $0.192/hr × 24 × 30 = $138.24/month
monthly_savings = $138.24/month
Cluster has 3 idle nodes (m5.xlarge):
total_monthly_waste = 3 × $138.24 = $414.72/month
total_annual_savings = $414.72 × 12 = $4,976.64/year
Karpenter Consolidation Savings (when consolidation is disabled)
# If Karpenter is installed but consolidateAfter is not set or consolidation is off
consolidation_monthly_savings = (
idle_node_count * avg_instance_hourly_rate * 24 * 30 * 0.25
)
# 25% is conservative — actual savings depend on workload packing density
3. Storage Waste
3a. gp2 → gp3 Migration Savings
Inputs
| Input | Source | Example |
|---|---|---|
provisioned_gb | PVC .spec.resources.requests.storage | 100 GiB |
storage_class | PVC .spec.storageClassName | gp2 |
Pricing
| Storage Class | Cost per GiB/month |
|---|---|
| gp2 | $0.10 |
| gp3 | $0.08 |
| Savings | $0.02/GiB/month (20%) |
Calculation
# Per volume
gp2_monthly_cost = provisioned_gb * 0.10
gp3_monthly_cost = provisioned_gb * 0.08
monthly_waste = gp2_monthly_cost - gp3_monthly_cost # = provisioned_gb * 0.02
monthly_savings = monthly_waste # full savings, zero-effort migration
# Aggregate across all gp2 volumes
total_gp2_waste = sum(pvc.provisioned_gb for pvc in gp2_pvcs) * 0.02
Worked Example
Cluster has 12 gp2 PVCs totaling 850 GiB:
monthly_waste = 850 × $0.02 = $17.00/month
annual_savings = $17.00 × 12 = $204.00/year
Individual PVC: data-postgres-0 (500 GiB, gp2)
monthly_waste = 500 × $0.02 = $10.00/month
monthly_savings = $10.00/month
effort: Low (create gp3 StorageClass, migrate PVC)
3b. Unused PVC Waste
Inputs
| Input | Source | Example |
|---|---|---|
pvc_status | PVC .status.phase | Bound |
pvc_provisioned_gb | PVC .spec.resources.requests.storage | 50 GiB |
storage_class | PVC .spec.storageClassName | gp3 |
last_mount_time | Pod events / volume attachment events | 14 days ago |
storage_rate | Rate for the storage class | $0.08/GiB/month |
Detection Criteria
A PVC is unused if:
- Status is
Boundbut no pod has mounted it in the last 7 days, OR - Status is
Released(volume freed but PVC object remains)
Calculation
# Per unused PVC — the FULL cost is waste since nothing uses it
monthly_waste = pvc_provisioned_gb * storage_rate
monthly_savings = monthly_waste # delete PVC to recover full cost
Worked Example
PVC: logging/elasticsearch-data-2
Status: Bound
Size: 200 GiB
Storage class: gp3 ($0.08/GiB/month)
Last mounted: 21 days ago (pod was deleted)
Calculation:
monthly_waste = 200 × $0.08 = $16.00/month
monthly_savings = $16.00/month
Cluster has 4 unused PVCs totaling 500 GiB (gp3):
total_monthly_waste = 500 × $0.08 = $40.00/month
3c. Oversized Volume Waste
Inputs
| Input | Source | Example |
|---|---|---|
provisioned_gb | PVC .spec.resources.requests.storage | 500 GiB |
actual_used_gb | CloudWatch VolumeUsedBytes / kubelet volume stats | 45 GiB |
storage_rate | Rate for the storage class | $0.08/GiB/month |
Detection Criteria
Flag if:
waste_ratio > 50%ANDprovisioned_gb > 20 GiB(ignore small volumes)
Calculation
waste_ratio = (provisioned_gb - actual_used_gb) / provisioned_gb
wasted_gb = provisioned_gb - actual_used_gb
# Can't always shrink EBS volumes, so savings = cost of excess capacity
monthly_waste = wasted_gb * storage_rate
# Savings assumes right-sizing to 2x actual usage (safety buffer)
right_sized_gb = max(actual_used_gb * 2, 20) # minimum 20 GiB
monthly_savings = (provisioned_gb - right_sized_gb) * storage_rate
Worked Example
PVC: analytics/clickstream-data
Provisioned: 500 GiB (gp3)
Actual used: 45 GiB
Storage rate: $0.08/GiB/month
Detection:
waste_ratio = (500 - 45) / 500 = 91%
91% > 50% ✓ AND 500 > 20 GiB ✓ → OVERSIZED
Calculation:
wasted_gb = 500 - 45 = 455 GiB
monthly_waste = 455 × $0.08 = $36.40/month
right_sized_gb = max(45 × 2, 20) = 90 GiB
monthly_savings = (500 - 90) × $0.08 = $32.80/month
effort: Medium (requires volume snapshot + restore to smaller size)
4. Network Waste
4a. Cross-AZ Data Transfer
Inputs
| Input | Source | Example |
|---|---|---|
cross_az_gb_per_month | VPC Flow Logs / Container Insights network metrics | 500 GiB |
cross_az_rate | AWS pricing (fixed) | $0.01/GiB |
Calculation
# Current cross-AZ cost
monthly_waste = cross_az_gb_per_month * 0.01
# Savings from topology-aware routing (typically 50–80% reduction)
reduction_factor = 0.65 # conservative 65% reduction
monthly_savings = monthly_waste * reduction_factor
Worked Example
Cluster: production-us-east-1
Pods spread across 3 AZs (us-east-1a, 1b, 1c)
Service "order-api" has 6 replicas across all 3 AZs
Estimated cross-AZ traffic: 500 GiB/month
Calculation:
monthly_waste = 500 × $0.01 = $5.00/month
monthly_savings = $5.00 × 0.65 = $3.25/month
fix: Enable topology-aware routing on high-traffic services
effort: Low (add annotation to Service)
Estimating Cross-AZ Traffic
When direct metrics are unavailable, estimate from service topology:
# For each Service with pods in multiple AZs:
# Assume uniform traffic distribution across replicas
# Traffic that hits a replica in a different AZ = cross-AZ
num_azs = len(set(pod.az for pod in service.pods))
cross_az_probability = (num_azs - 1) / num_azs # e.g., 2/3 for 3 AZs
# If service handles ~1000 requests/sec at ~10KB avg response:
estimated_monthly_gb = (requests_per_sec * avg_response_kb / 1024 / 1024) * 86400 * 30
cross_az_gb = estimated_monthly_gb * cross_az_probability
4b. NAT Gateway Waste
Inputs
| Input | Source | Example |
|---|---|---|
nat_gb_per_month | CloudWatch NatGatewayBytesOutToDestination | 200 GiB |
nat_rate | AWS pricing (fixed) | $0.045/GiB |
num_azs | Number of AZs with private subnets | 3 |
vpc_endpoint_hourly_rate | AWS pricing (fixed) | $0.01/hr/AZ |
Calculation
# Current NAT cost for AWS service traffic
monthly_waste = nat_gb_per_month * 0.045
# VPC endpoint cost (per endpoint, per AZ)
endpoint_monthly_cost = vpc_endpoint_hourly_rate * 24 * 30 * num_azs
# = $0.01 × 24 × 30 × 3 = $21.60/month per endpoint
# Savings = NAT cost eliminated minus endpoint cost
# Typically need endpoints for: ECR (2 endpoints), S3 (gateway, free), STS (1)
num_interface_endpoints = 3 # ecr.api, ecr.dkr, sts
total_endpoint_cost = endpoint_monthly_cost * num_interface_endpoints
monthly_savings = monthly_waste - total_endpoint_cost
# Only recommend if savings > 0 (break-even analysis)
Worked Example
Cluster: production-us-east-1
NAT Gateway traffic to AWS services: 200 GiB/month
AZs: 3
No VPC endpoints configured
Current NAT cost:
monthly_waste = 200 × $0.045 = $9.00/month
VPC endpoint cost (if added):
S3 gateway endpoint: FREE
ECR endpoints (ecr.api + ecr.dkr): 2 × ($0.01 × 24 × 30 × 3) = $43.20/month
STS endpoint: 1 × ($0.01 × 24 × 30 × 3) = $21.60/month
Total endpoint cost: $64.80/month
Break-even analysis:
$9.00 < $64.80 → VPC endpoints NOT cost-effective at this traffic level
→ Only recommend VPC endpoints when NAT traffic > ~1,440 GiB/month
→ Break-even: nat_gb × $0.045 > num_endpoints × $21.60
→ Break-even GB = (3 × $21.60) / $0.045 = 1,440 GiB/month
Higher-traffic example (2,000 GiB/month):
monthly_waste = 2,000 × $0.045 = $90.00/month
endpoint_cost = $64.80/month
monthly_savings = $90.00 - $64.80 = $25.20/month ✓
5. Spot Opportunity
Inputs
| Input | Source | Example |
|---|---|---|
workload_is_stateless | No PVCs, no StatefulSet | true |
workload_replicas | Deployment .spec.replicas | 4 |
workload_has_pdb | PodDisruptionBudget exists | true |
workload_node_type | Node capacity-type label | on-demand |
workload_monthly_cost | Allocated node cost for this workload | $280.00 |
spot_discount | Typical Spot discount (region/instance dependent) | 0.65 (65% off) |
Spot Eligibility Criteria
A workload is Spot-eligible if ALL of:
- Stateless (no PersistentVolumeClaims)
- Not a database or stateful system (not a StatefulSet)
- Has multiple replicas (replicas > 1)
- Has a PodDisruptionBudget configured
- Currently running on On-Demand nodes
Getting Accurate Spot Pricing
The default 65% discount is a conservative estimate. For production-grade findings, query actual Spot prices for the customer's instance types and region:
Use the EC2 DescribeSpotPriceHistory API to get current Spot pricing for the target instance types and AZs in the customer's region.
When live Spot pricing is available:
- Use
actual_spot_discount = 1 - (spot_price / on_demand_price)per instance type - Confidence level: High
- Report exact dollar savings
When live Spot pricing is NOT available (no ec2:DescribeSpotPriceHistory permission):
- Use 65% default discount
- Confidence level: Medium
- Prefix savings with "~" (approximate)
Calculation
# Identify Spot-eligible workloads currently on On-Demand
spot_eligible = [
w for w in workloads
if w.is_stateless
and w.replicas > 1
and w.has_pdb
and w.node_capacity_type == "on-demand"
and not w.is_statefulset
]
# Per workload
on_demand_cost = workload_monthly_cost
spot_savings = on_demand_cost * spot_discount # typically 60–70%
monthly_waste = spot_savings # "waste" = premium paid for On-Demand
monthly_savings = spot_savings
# Aggregate
total_spot_opportunity = sum(w.monthly_savings for w in spot_eligible)
Worked Example
Workload: frontend/web-app
Replicas: 6
Stateless: yes (no PVCs)
PDB: yes (minAvailable: 4)
Current capacity: On-Demand (m5.xlarge nodes)
Monthly cost: $280.00
Spot discount for m5.xlarge in us-east-1: ~65%
Calculation:
monthly_waste = $280.00 × 0.65 = $182.00/month
monthly_savings = $182.00/month
Cluster has 5 Spot-eligible workloads:
| Workload | Monthly Cost | Spot Savings |
|----------|-------------|--------------|
| frontend/web-app | $280 | $182 |
| api/gateway | $420 | $273 |
| workers/processor | $560 | $364 |
| cache/redis-proxy | $140 | $91 |
| monitoring/collector | $96 | $62 |
| **Total** | **$1,496** | **$972/month** |
total_annual_savings = $972 × 12 = $11,664/year
effort: Medium (create Spot NodePool, add tolerations, verify PDBs)
6. Graviton Opportunity
Inputs
| Input | Source | Example |
|---|---|---|
workload_architecture | Node label kubernetes.io/arch | amd64 |
image_supports_arm64 | docker manifest inspect or ECR image index | true |
workload_monthly_cost | Allocated node cost | $192.00 |
graviton_discount | Graviton vs x86 price differential | 0.20 (20% cheaper) |
Graviton Eligibility Criteria
A workload is Graviton-eligible if:
- Currently running on x86 (amd64) nodes
- Container image supports arm64 platform (multi-arch manifest)
- No x86-specific binary dependencies (e.g., custom native libraries compiled for x86 only)
Calculation
# Identify Graviton-eligible workloads on x86
graviton_eligible = [
w for w in workloads
if w.current_arch == "amd64"
and w.image_supports_arm64
and not w.requires_x86_specific_features
]
# Per workload
x86_cost = workload_monthly_cost
graviton_savings = x86_cost * graviton_discount # typically 20%
monthly_waste = graviton_savings # "waste" = premium paid for x86
monthly_savings = graviton_savings
# Aggregate
total_graviton_opportunity = sum(w.monthly_savings for w in graviton_eligible)
Checking arm64 Support
Check container image manifest for multi-architecture support (arm64 availability) via the container registry API or ECR BatchGetImage API to inspect the image manifest list.