Cloud Waste Detection Guide

Typical Waste
15–32%
of cloud spend
Found in Audit
3–7%
can be reclaimed instantly
Annual Savings
$10K–$500K
typical mid-size
Audit Frequency
Weekly
recommended

Cloud waste detection is the first step in FinOps. This guide walks through a systematic 15-point audit using native cloud tooling. Run this audit weekly — most waste is recoverable within 24 hours of detection.

01 Idle / Stopped Instances Still Charging [ HIGH PRIORITY ]

Even stopped instances can incur charges if attached EBS volumes are present. Verify every "stopped" instance.

$ aws ec2 describe-instances --query 'Reservations[].Instances[?State.Name==`stopped`]'
$ aws ec2 describe-instances \
    --filters "Name=instance-state-name,Values=stopped" \
    --query 'Instances[*].[InstanceId,InstanceType,Tags[?Key==`Name`].Value|[0],BlockDeviceMappings[*].Ebs.VolumeId]' \
    --output table

┌─────────────────┬──────────────┬─────────────────────┬────────────────┐
│  InstanceId     │  Type        │  Name                │  Attached-Vol  │
├─────────────────┼──────────────┼─────────────────────┼────────────────┤
│  i-0a1b2c3d4e   │  r5.xlarge   │  legacy-db-backup    │  vol-0abc123   │
│  i-0a1b2c3d4f   │  t3.medium   │  test-env-2023       │  vol-0def456   │
│  i-0a1b2c3d4g   │  m5.large    │  old-web-server      │  none
└─────────────────┴──────────────┴─────────────────────┴────────────────┘

⚠ i-0a1b2c3d4e — r5.xlarge @ $0.483/hr stopped but still charged (EBS attached)
⚠ i-0a1b2c3d4f — t3.medium @ $0.042/hr — test environment from 2023, unused
  • Identify all stopped instances with attached EBS volumes
  • Review each: is this instance needed for disaster recovery?
  • Snapshot critical volumes before termination
  • Terminate or create AMI for archival if not needed
  • 02 Orphaned EBS Volumes [ IMMEDIATE ACTION ]

    Volumes attached to terminated instances generate charges with zero value. This is one of the most common sources of silent waste.

    $ aws ec2 describe-volumes --filters "Name=status,Values=available"
    $ aws ec2 describe-volumes \
        --filters "Name=status,Values=available" \
        --query 'Volumes[*].[VolumeId,VolumeType,Size,CreateTime,Tags[?Key==`Name`].Value|[0]]' \
        --output table
    
    ┌──────────────┬──────────┬──────┬──────────────────────┬───────────────────┐
    │  VolumeId    │  Type    │  GiB │  CreateDate          │  Name             │
    ├──────────────┼──────────┼──────┼──────────────────────┼───────────────────┤
    │  vol-0abc123 │  gp3     │  100 │  2023-06-15T09:00:00 │  old-server-root  │
    │  vol-0def456 │  st1     │  500 │  2022-11-01T14:22:00 │  data-archive-old │
    │  vol-0ghi789 │  gp3     │  50  │  2024-01-10T11:00:00 │  unnamed-$%#      │
    └──────────────┴──────────┴──────┴──────────────────────┴───────────────────┘
    
    ⚠ 3 orphaned volumes × avg $0.08/GiB/mo = $52.80/mo wasted
    ⚠ vol-0ghi789 — no tags, no name, likely test artifact
    gp3 cost: $0.08/GiB/mo + $0.005/provisioned IOPS/mo. A 100 GiB gp3 with 3000 IOPS = $15/mo even with zero activity.
    [ AdSense Slot 1 — mid Waste Detection ]
    03 Unattached Elastic IPs [ $3.65/address/hr ]

    AWS charges $0.005/hr (~$3.65/mo) for Elastic IPs not associated with a running instance. Every unassociated EIP is a monthly leak.

    $ aws ec2 describe-addresses --query 'Addresses[?Association[?InstanceId]==`null`]'
    $ aws ec2 describe-addresses \
        --query 'Addresses[?Association.InstanceId==null].[PublicIp,AllocationId]' \
        --output json | jq '.'
    
    [
      { "PublicIp": "52.15.67.89",  "AllocationId": "eipalloc-abc123" },
      { "PublicIp": "18.218.90.12", "AllocationId": "eipalloc-def456" },
      { "PublicIp": "3.19.45.67",   "AllocationId": "eipalloc-ghi789" }
    ]
    
    ⚠ 3 unattached EIPs × $3.65/mo = $10.95/mo wasted
    → Release with: aws ec2 release-address --allocation-id eipalloc-xxxxx
    04 Oversized Instances [ RIGHT-SIZE TARGET ]

    Use AWS Compute Optimizer (or Azure Advisor / GCP Recommender) to find instances with CPU < 40% over 30 days. Each downsizing saves 30–60% per instance.

    $ aws compute-optimizer get-instance-recommendations
    $ aws compute-optimizer get-instance-recommendations \
        --account-id 123456789012 \
        --query 'instanceRecommendations[*].[accountId,instanceArn,currentInstanceType,recommendedInstanceType,findings]' \
        --output table
    
    ┌─────────────────┬──────────────────────┬────────────────────┬──────────────────────┬───────────┐
    │  Account        │  InstanceArn         │  CurrentType       │  RecommendedType     │  Finding  │
    ├─────────────────┼──────────────────────┼────────────────────┼──────────────────────┼───────────┤
    │  123456789012   │  i-0abc123def        │  r5.4xlarge        │  r5.xlarge           │  OVERDIZED│
    │  123456789012   │  i-0def456ghi        │  m5.2xlarge        │  m5.large            │  OVERDIZED│
    │  123456789012   │  i-0ghi789jkl        │  t3.large           │  t3.micro            │  OVERDIZED│
    └─────────────────┴──────────────────────┴────────────────────┴──────────────────────┴───────────┘
    
    Estimated monthly savings (3 instances):
    $847.20 → $318.40  (save $528.80/mo)
    Right-sizing wins: 3 instances, save $528.80/mo = $6,345/yr. Test at target size before terminating originals.
    05 Old Snapshots Without Tags [ STORAGE LEAK ]
    $ aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[*].[SnapshotId,VolumeSize,StartTime]'
    $ aws ec2 describe-snapshots \
        --owner-ids self \
        --query 'Snapshots[*].[SnapshotId,VolumeSize,StartTime,Tags[?Key==`Name`].Value|[0]]' \
        --output table | sort -k3
    
    ┌──────────────┬──────┬────────────────────┬─────────────────┐
    │  SnapshotId  │  GiB │  Created           │  Name           │
    ├──────────────┼──────┼────────────────────┼─────────────────┤
    │  snap-abc123 │  500 │  2022-03-15        │  old-backup-1   │
    │  snap-def456 │  200 │  2022-07-01        │  (none)
    │  snap-ghi789 │  100 │  2024-02-20        │  current-web    │
    └──────────────┴──────┴────────────────────┴─────────────────┘
    
    ⚠ snap-def456 — no tags, 200 GiB from 2022. Verify before deletion.
    06 Data in Wrong Storage Tier [ LIFECYCLE POLICY ]

    Use S3 Storage Lens or cost allocation reports to find buckets with objects older than the appropriate tier threshold.

    Access PatternCorrect TierWrong TierMisplaced Cost
    Quarterly accessGlacierHot~$23/TB/yr overpay
    Monthly accessCool/IAHot~$20/TB/yr overpay
    Unused (>90 days)GlacierStandard~$21/TB/yr overpay
    $ aws s3api list-objects-v2 --bucket my-bucket --query 'Contents[?LastModified < `2024-01-01`]
    $ aws s3api list-objects-v2 \
        --bucket my-log-bucket \
        --query 'Contents[?LastModified < `2024-01-01`].[Key,Size,StorageClass]' | \
        jq '.[] | "Objects over 1yr old: \(.[0]) — \(.[1]) bytes — \(.[2])"
    
    Objects over 1yr old: logs/2022/Q4/app.log — 4.2 GB — STANDARD
    Objects over 1yr old: logs/2022/Q4/error.log — 0.8 GB — STANDARD
    Objects over 1yr old: exports/2023/backups/ — 120 GB — STANDARD
    → Apply lifecycle rule: S3 bucket → Management → Lifecycle → Move to Glacier after 365d
    07 Lapsed RI / Savings Plan Coverage [ COMMITMENT GAP ]
    $ aws ce get-reservation-coverage --time-period 2024-01-01,2024-12-31
    $ aws ce get-reservation-coverage \
        --time-period Start=2024-01-01,End=2024-12-31 \
        --metrics Coverage_percentage,OnDemandCost,TotalRunningHours \
        --granularity MONTHLY \
        --query 'CoverageByInstanceType[].[InstanceType,Coverage.RunningHours,Coverage.CoverageHoursPercentage]'
    
    ┌────────────────┬────────────────┬────────────────────┐
    │  InstanceType  │  RunningHours   │  Coverage%         │
    ├────────────────┼────────────────┼────────────────────┤
    │  t3.micro       │  43,800         │  12%
    │  t3.small       │  43,800         │  8%
    │  m5.large       │  43,800         │  82%
    └────────────────┴────────────────┴────────────────────┘
    
    ⚠ t3.micro + t3.small — 88–92% on-demand. Buying 1-yr SP would save ~30%.
    [ AdSense Slot 2 — bottom Waste Detection ]
    15 Complete Waste Detection Checklist [ SUMMARY ]
  • Stopped instances with attached EBS volumes
  • Orphaned EBS volumes (available state)
  • Unattached Elastic IP addresses
  • Oversized instances (CPU < 40% avg over 30d)
  • Old snapshots without tags or purpose
  • Data in wrong storage tier (Hot instead of Cool/Glacier)
  • Lapsed or insufficient RI/SP coverage
  • Non-production instances running 24/7 (should be scheduled)
  • Unused NAT Gateways or Transit Gateways
  • Classic Load Balancers still in use (should migrate to ALB/NLB)
  • Unused VPC endpoints
  • Over-provisioned RDS / Aurora instances
  • Elastic Beanstalk environments not terminated after migration
  • CloudWatch custom metrics from deleted resources
  • Unused Secrets Manager secrets or KMS keys
  • First-run priority: Items 1–3 (immediate zero-cost wins), then 4–6 (requires planning), then 7–15 (ongoing governance).
    Disclaimer: This guide provides general informational content about cloud infrastructure cost management. Figures and benchmarks are based on publicly available industry averages (e.g., Gartner, IDC, cloud provider documentation) and may vary by provider, region, and workload. This content is not a substitute for professional financial, legal, or technical advice specific to your organization.