Systematically identify and eliminate cloud waste — idle instances, unattached volumes, oversized databases, and forgotten environments. Covers detection patterns, automated cleanup, tagging hygiene, and building a waste-aware culture across engineering teams.
Cloud waste detection identifies and eliminates idle or underutilized cloud resources—such as unattached volumes, stopped instances, unused load balancers, and dormant databases—that consume money without delivering value. It matters when cloud spend grows faster than workload demand, and teams begin to notice recurring costs for resources that are never touched. This process is essential during onboarding of new services, after scaling spikes, and in the routine health checks of a mature cloud environment.
Find idle resources by measuring utilization over time and comparing it against thresholds. A resource is idle when its usage remains below a baseline for a defined period. The baseline may be a percentage of capacity, an absolute metric, or a combination of both.
An EC2 instance is idle when CPU utilization is below 10% for at least 72 consecutive hours. Use the CloudWatch API to retrieve metrics and detect such instances.
curl \
--header "Authorization: Bearer $AWS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"MetricDataQueries": [
{
"Id": "cpu",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "CPUUtilization",
"Dimensions": [
{ "Name": "InstanceId", "Value": "i-1234567890abcdef0" }
]
},
"Period": 3600,
"Stat": "Average"
},
"ReturnData": true
}
],
"StartTime": "2023-10-01T00:00:00Z",
"EndTime": "2023-10-04T00:00:00Z"
}' \
https://monitoring.us-east-1.amazonaws.com
The response includes a Datapoints array. If the Average value is less than 10 for 72 or more consecutive data points, the instance qualifies as idle.
When fetching multiple instances, use a batched request:
curl \
--header "Authorization: Bearer $AWS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"MetricDataQueries": [
{
"Id": "cpu",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "CPUUtilization",
"Dimensions": [
{ "Name": "InstanceId", "Value": "i-1234567890abcdef0" }
]
},
"Period": 3600,
"Stat": "Average"
},
"ReturnData": true
},
{
"Id": "network",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "NetworkIn",
"Dimensions": [
{ "Name": "InstanceId", "Value": "i-1234567890abcdef0" }
]
},
"Period": 3600,
"Stat": "Sum"
},
"ReturnData": true
}
],
"StartTime": "2023-10-01T00:00:00Z",
"EndTime": "2023-10-04T00:00:00Z"
}' \
https://monitoring.us-east-1.amazonaws.com
The Period of 3600 seconds (1 hour) ensures hourly granularity. A single DataPoint is missing if the instance is stopped during the interval. This causes a spike in NetworkIn but a flat CPUUtilization, leading to false positives.
An EBS volume is idle if it has no I/O for 14 days. Use the GetMetricData API to retrieve ReadOps and WriteOps over time.
{
"MetricDataQueries": [
{
"Id": "read",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "ReadOps",
"Dimensions": [
{ "Name": "VolumeId", "Value": "vol-1234567890abcdef0" }
]
},
"Period": 86400,
"Stat": "Sum"
},
"ReturnData": true
},
{
"Id": "write",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "WriteOps",
"Dimensions": [
{ "Name": "VolumeId", "Value": "vol-1234567890abcdef0" }
]
},
"Period": 86400,
"Stat": "Sum"
},
"ReturnData": true
}
],
"StartTime": "2023-09-15T00:00:00Z",
"EndTime": "2023-10-01T00:00:00Z"
}
If both ReadOps and WriteOps are zero for 14 consecutive days, the volume is idle. A volume may be attached but unused if it’s part of a backup pipeline that runs weekly. The Sum statistic captures total operations per day; a Sum of zero means no I/O.
A PostgreSQL instance is idle when pg_stat_database reports tup_returned and tup_fetched at zero for 7 days.
SELECT datname,
(tup_returned + tup_fetched) AS total_rows,
(EXTRACT(EPOCH FROM now() - last_autovacuum) / 86400) AS days_since_autovacuum
FROM pg_stat_database
WHERE (tup_returned + tup_fetched) = 0
AND last_autovacuum < now() - interval '7 days';
The query returns all databases with no read or write activity. A database with tup_returned at zero is effectively idle. If tup_fetched is also zero, it has not been accessed since last autovacuum.
An ALB is idle when RequestCount is less than 10 per day for 14 consecutive days. Use CloudWatch RequestCount metric with Period of 86400.
{
"MetricDataQueries": [
{
"Id": "request-count",
"MetricStat": {
"Metric": {
"Namespace": "AWS/ApplicationELB",
"MetricName": "RequestCount",
"Dimensions": [
{ "Name": "LoadBalancer", "Value": "app/my-alb/1234567890abcdef" }
]
},
"Period": 86400,
"Stat": "Sum"
},
"ReturnData": true
}
],
"StartTime": "2023-09-15T00:00:00Z",
"EndTime": "2023-10-01T00:00:00Z"
}
The Sum over 86400 seconds (1 day) gives total requests per day. A Sum less than 10 for 14 days indicates low usage. A common failure: an ALB is created for a staging environment but never receives traffic. The RequestCount metric remains flat at zero, but HealthyHostCount may be non-zero due to target group health checks, leading to a misclassification.
After identifying idle resources, classify them by duration and cost impact.
Resources idle for less than 7 days but expected to be active soon. These include:
For such resources, the cost of keeping them alive outweighs the benefit of quick startup.
Resources idle for 7 to 30 days. These are typically:
They can be transitioned to a lower tier or scheduled to start/stop.
Resources idle for more than 30 days. These are:
They are prime candidates for termination.
Eliminate waste by automating the lifecycle of idle resources. This involves three steps: signal, action, and verification.
Use event-driven systems to detect idle resources. A common pattern is to run a daily job that queries metrics and emits events.
#!/bin/bash
# Query CloudWatch for idle EC2 instances
AWS_TOKEN=$(aws sts assume-role --role-arn arn:aws:iam::123456789012:role/CloudWasteDetector --query 'Credentials.AccessToken' --output text)
# Fetch CPU and network metrics for all instances
for instance_id in $(aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text); do
response=$(curl \
--header "Authorization: Bearer $AWS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"MetricDataQueries": [
{
"Id": "cpu",
"MetricStat": {
"Metric": {
"Namespace": "AWS/EC2",
"MetricName": "CPUUtilization",
"Dimensions": [
{ "Name": "InstanceId", "Value": "'"$instance_id"'" }
]
},
"Period": 3600,
"Stat": "Average"
},
"ReturnData": true
}
],
"StartTime": "2023-10-01T00:00:00Z",
"EndTime": "2023-10-04T00:00:00Z"
}' \
https://monitoring.us-east-1.amazonaws.com)
# Extract average CPU
avg_cpu=$(echo "$response" | jq -r '.MetricDataResults[0].Values[0]')
# Check if average CPU < 10
if (( $(echo "$avg_cpu < 10" | bc -l) )); then
echo "Idle instance: $instance_id"
# Emit event to message queue
echo "{
\"resource_type\": \"EC2\",
\"instance_id\": \"$instance_id\",
\"reason\": \"CPU < 10% for 72 hours\",
\"detected_at\": \"$(date -Iseconds)\"
}" | kafka-console-producer.sh --broker-list kafka.example.com:9092 --topic cloud-waste-identified
fi
done
This script runs daily as a cron job. It emits structured events to a Kafka topic. Each event includes the resource type, identifier, reason for being idle, and detection timestamp.
Based on the signal, apply policies to reduce or remove waste.
Create a policy that stops instances after 7 days of idle CPU.
{
"name": "stop-idle-instances",
"schedule": "0 2 * * *",
"actions": [
{
"type": "stop-instances",
"filter": {
"metric": "CPUUtilization",
"threshold": 10,
"duration": "7d",
"period": "1h"
},
"parameters": {
"force": true,
"deregister": false,
"stop-time": "2023-10-04T02:00:00Z"
}
}
]
}
The policy runs every day at 2:00 AM. It checks for instances with CPUUtilization below 10% over a 7-day period, with hourly data. It stops them, forces the stop even if the instance is in use, and records the stop time.
For resources idle for more than 30 days, schedule termination.
{
"name": "terminate-long-idle-resources",
"schedule": "0 3 1 * *",
"actions": [
{
"type": "terminate-resources",
"filter": {
"metric": "RequestCount",
"threshold": 10,
"duration": "14d",
"period": "1d"
},
"parameters": {
"dry-run": false,
"reason": "Long-term idle: terminated via cloud-waste automation"
}
}
]
}
This policy runs monthly on the first day of the month. It identifies ALBs with less than 10 requests per day for 14 days and terminates them.
After applying actions, verify that waste has been eliminated.
After a termination, monitor RequestCount and CPUUtilization to ensure the resource is no longer needed.
{
"type": "verify-termination",
"query": "SELECT count(*) FROM cloud_waste_events WHERE action = 'terminate' AND status = 'success'",
"threshold": 1,
"timeout": "30m",
"check": {
"metric": "RequestCount",
"expected": 0,
"duration": "7d",
"period": "1d"
}
}
The verification job runs after each termination. It checks that the RequestCount remains at zero for 7 days. If not, it logs a failure.
Common failure modes in waste elimination:
CPUUtilization as null for a day. This happens when the instance is stopped during the metric collection window.ReadOps and WriteOps at zero, but the instance is not mounting it. The VolumeId is correct, but fstab or mount command is missing.tup_returned at zero, but a nightly job runs VACUUM and ANALYZE. The tup_fetched is non-zero due to ANALYZE, leading to a false classification.HealthyHostCount at 2, but RequestCount is zero. This occurs when the health check interval is shorter than the request arrival interval.Waste detection is not one-size-fits-all. Use patterns to handle edge cases.
Instead of reprocessing all data every time, use incremental updates.
timestamp of processed data.MetricData starting from that timestamp.last_processed timestamp after each run.This reduces cost and latency.
Combine multiple metrics into a weighted score.
{
"type": "weighted-idle",
"metrics": [
{
"name": "CPUUtilization",
"weight": 0.5,
"threshold": 10,
"duration": "7d",
"period": "1h"
},
{
"name": "NetworkIn",
"weight": 0.3,
"threshold": 1000000,
"duration": "7d",
"period": "1h"
},
{
"name": "ReadOps",
"weight": 0.2,
"threshold": 0,
"duration": "14d",
"period": "1d"
}
],
"threshold": 1.2,
"score": "sum"
}
A resource is idle if the total score exceeds 1.2. This allows a resource to be considered idle even if one metric is slightly off.
Cloud waste detection is the continuous process of identifying and eliminating underutilized resources. It begins with measuring utilization across compute, storage, databases, and networking. Resources are classified by duration and impact. Policies are applied to stop, resize, or terminate them. Verification ensures success. The mechanism is flexible, scalable, and resilient to edge cases. It works across platforms and tools, relying on standard APIs, metrics, and event formats. The key is not just detection, but action—transforming insight into cost reduction.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy