Cloud infrastructure bills regularly outpace application traffic growth. In Azure Kubernetes Service (AKS) environments, engineering teams frequently over-provision CPU and memory requests to prevent sporadic pod evictions, misconfigure database connection pools across dozens of microservices, and leave idle storage attached to running clusters. A structured cloud cost and performance audit uncovers these systemic inefficiencies and reduces monthly cloud expenditure without risking workload stability.
Phase 1: Diagnosing Pod Resource Waste with Prometheus and VPA
Kubernetes schedules workloads based on pod resource requests, not actual usage. When an engineering team sets high safety buffers (such as requesting 2 CPU cores and 4 GB RAM for an API service that averages 0.15 cores and 600 MB RAM), the Kubernetes scheduler reserves those resources on the node. The node reports high allocation even though the underlying hardware sits idle, forcing the Azure cluster autoscaler to spin up expensive additional virtual machines.
| Resource Field | Kubernetes Scheduler Behavior | Runtime Enforcement & Cost Impact |
|---|---|---|
requests.cpu |
Reserves node capacity during scheduling decisions | Determines total node count and direct Azure VM cost |
limits.cpu |
Ignored by the scheduler | Enforces CFS throttling quotas; excessive throttling degrades P99 latency |
requests.memory |
Reserves physical RAM capacity on the worker node | Prevents node overcommit; drives node pool sizing |
limits.memory |
Ignored during scheduling | Triggers Linux cgroup Out-Of-Memory (OOM) killer terminating the container |
Step 1: Quantify CPU and Memory Slack with PromQL
Audit container resource allocation by comparing requested capacity against actual 95th percentile consumption over a 14-day window:
# PromQL: Calculate CPU Request Slack per Deployment
sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace, container)
- sum(quantile_over_time(0.95, container_cpu_usage_seconds_total[14d])) by (namespace, container)
# PromQL: Calculate Memory Request Over-Provisioning Ratio
(sum(kube_pod_container_resource_requests{resource="memory"}) by (namespace, container)
- sum(quantile_over_time(0.95, container_memory_working_set_bytes[14d])) by (namespace, container))
/ sum(kube_pod_container_resource_requests{resource="memory"}) by (namespace, container)
Workloads showing over 50% persistent memory or CPU slack should have their baseline pod specs adjusted.
Step 2: Collect Recommendations with Kubernetes Vertical Pod Autoscaler (VPA)
Deploy the Kubernetes Vertical Pod Autoscaler in Off mode. In this mode, VPA calculates historical usage percentiles and writes right-sizing recommendations to the resource status without restarting running pods.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: order-processing-service-vpa
namespace: production
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: order-processing-service
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed:
cpu: 100m
memory: 256Mi
maxAllowed:
cpu: 2000m
memory: 4Gi
controlledResources: ["cpu", "memory"]
Query the recommendations using kubectl get vpa -n production -o yaml. Compare the target and lowerBound against current manifests to calculate potential node reductions.
Phase 2: Database Connection Pooling Traps and Query Bottlenecks
In distributed .NET microservices on AKS, database connection management causes severe cost escalation. Teams scale out pod replicas during traffic spikes, unknowingly multiplying active database connections until the database engine throttles or runs out of resources.
The Multiplier Trap
Default ADO.NET and Npgsql connection pools allow up to 100 connections per process. When a service scales from 3 to 30 pods under load, potential connections jump from 300 to 3,000. An Azure SQL Database or PostgreSQL Flexible Server hits connection limits, forcing engineering to upgrade to costly higher tiers simply for connection memory.
The SNAT Port Exhaustion Trap
Opening short-lived database connections or unpooled outbound HTTP clients consumes Azure Load Balancer SNAT ports. Under high load, port exhaustion manifests as intermittent connection timeouts, masking database health behind networking errors.
Remediating Connection Pools in .NET Workloads
Restrict client-side pool sizes and insert a dedicated server-side connection pooler (such as PgBouncer for PostgreSQL or Azure SQL Database Elastic Pools) between container pods and the database instance:
// appsettings.Production.json
{
"ConnectionStrings": {
"PrimaryDatabase": "Server=tcp:sql-neneos-prod.database.windows.net,1433;Database=orders;Authentication=Active Directory Default;Max Pool Size=20;Min Pool Size=2;Connection Timeout=15;Pooling=true;"
}
}
Setting Max Pool Size=20 per pod keeps total connections under 400 even when the deployment scales to 20 replicas, allowing the database instance to run on lower vCore tiers with stable memory consumption.
Eliminating Costly Unindexed Queries
Missing indexes force database engines into full table scans, consuming high IOPS and CPU. On Azure SQL and Azure Database for PostgreSQL, high compute usage often masks unindexed queries. Teams frequently upgrade from a 4-vCore instance ($280/month) to a 16-vCore instance ($1,120/month) to handle query spikes that an index would resolve.
Run index evaluation queries directly during the audit to uncover heavy CPU consumers:
-- Azure SQL / SQL Server: Top 5 Queries by Total Worker CPU Time
SELECT TOP 5
qs.total_worker_time / qs.execution_count AS AvgCpuTimeMicroseconds,
qs.execution_count,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
(((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END) - qs.statement_start_offset)/2) + 1) AS StatementText,
qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
ORDER BY qs.total_worker_time DESC;
Phase 3: Noisy-Neighbor Workloads and Storage Inefficiencies
Without explicit resource constraints and storage policies, background jobs and uncleaned artifacts create hidden costs across the cluster.
Noisy Neighbor Containment with Quality of Service (QoS)
Batch processing jobs and data synchronization tasks scheduled alongside low-latency customer APIs cause CPU starvation and network packet drops if QoS classes are unconfigured. Kubernetes assigns pod QoS based on how requests and limits match:
- Guaranteed: CPU and memory
requestsequallimits. The node kernel pins resources and avoids evicting these pods. Use this for primary customer-facing APIs. - Burstable:
requestsare lower thanlimits. Pods can consume extra headroom when available. Suitable for standard services with variable traffic. - BestEffort: No requests or limits specified. These pods are killed first during memory pressure. Do not allow BestEffort pods in production namespaces.
Apply LimitRanges to enforce mandatory baseline requests and limits across every namespace:
apiVersion: v1
kind: LimitRange
metadata:
name: core-limits
namespace: production
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 256Mi
type: Container
Pruning Orphaned Disks and Oversized Storage
When PersistentVolumeClaims (PVCs) or AKS worker nodes are deleted, underlying Azure Managed Disks may remain in an unattached state while continuing to accrue monthly storage charges.
Scan for unattached managed disks using the Azure CLI:
# List all unattached managed disks across your subscription
az disk list \
--query "[?managedBy==null].[id, resourceGroup, name, diskSizeGb, sku.name]" \
--output table
Phase 4: Concrete Azure Run-Rate Reductions
Once workloads are right-sized, implement infrastructure-level cost reductions across the cluster:
Node Pool Specialization & Spot VMs
Separate steady-state services from asynchronous background workers. Run background message consumers and batch tasks on Azure Spot VM node pools for discounts up to 70–80% compared to pay-as-you-go pricing.
Event-Driven Autoscaling (KEDA)
Replace static pod replica counts with KEDA scalers listening to Azure Service Bus or RabbitMQ queue depths. Scale consumer workloads to zero during off-peak hours and scale up only when messages arrive.
Log Analytics Ingestion Filtering
Azure Monitor and Container Insights can generate massive ingestion bills. Filter out high-volume HTTP 200 health check logs and verbose framework debug messages at the collector level.
Azure Savings Plans & Reservations
After eliminating compute waste and stabilizing baseline node counts, apply 1-year or 3-year Azure Savings Plans for Compute to lock in up to 40–60% discounts on the stable core cluster footprint.
Audit Checklist: Cloud Cost and Performance
Use this reference checklist during your quarterly architecture reviews and infrastructure audits:
Kubernetes & Compute
Databases & Networking
Storage, Logging & Commercials
Professional Cloud Architecture Assessments
Auditing a complex multi-cluster cloud estate requires deep inspection across application code, container orchestration, and cloud service configurations.
Our Software Architecture Assessment, Kubernetes & Cloud Native, and Azure Cloud Engineering services deliver detailed technical reviews of your infrastructure and codebase. We identify hidden cost drivers, eliminate performance bottlenecks, and design practical remediation roadmaps tailored to your engineering team.