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.
    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%.
    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).