AWS cost & waste audit
Summary
The scan ran 12 read-only checks and found 20 items worth reviewing.
The largest single opportunity is Unattached EBS volume (vol-4d2b74392f2aca509,
about $208.10/month). 1 fix is low-risk and can be done this week. Nothing was changed in the account: every fix below is a command for you to review and run.
Top opportunities
| Priority | What | Resource | Region | Saving / month |
|---|---|---|---|---|
| high | Unattached EBS volume 100 GiB io1, 3,000 provisioned IOPS (perf-test), not attached to any instance |
vol-4d2b74392f2aca509 | ap-south-1 | $208.10 |
| high | Unattached EBS volume 1000 GiB gp2 (forgotten-ml-dataset), not attached to any instance |
vol-e8b333159767c43f1 | us-east-1 | $100.00 |
| high | Instance looks idle t2.large (legacy-reporting): average CPU 2.7%, peak 7.5% over 13 days |
i-bd540e2a038f46d43 | ap-south-1 | $72.42 |
| high | Database has had no connections db.t3.medium postgres 100 GiB: 0 connections in 13 days |
analytics-copy | ap-south-1 | $63.96 |
| medium | Unattached EBS volume 500 GiB gp3 (migration-temp), not attached to any instance |
vol-18086aeeb8ae02f33 | ap-south-1 | $45.60 |
| medium | NAT gateway barely used moved 0.51 GB (in + out) in 14 days (VPC vpc-663763701c56d4e86) |
nat-fd7db26a016ea87b1 | ap-south-1 | $40.88 |
| medium | Unattached EBS volume 200 GiB gp2 (old-jenkins-data), not attached to any instance |
vol-2173027d8b7ee1d33 | ap-south-1 | $22.80 |
| medium | Load balancer has no targets application load balancer with 1 target group(s) and 0 registered targets |
old-promo-alb | ap-south-1 | $16.43 |
| medium | Instance stopped for a long time t3.large (old-build-agent) stopped for 75 days, 1 volume(s) attached |
i-1a1444233b43272f1 | ap-south-1 | $13.68 |
| medium | Old snapshot not used by any AMI 410 days old, from a 200 GiB volume. pre-migration backup 2025 |
snap-8f13d4c9317c14b7e | ap-south-1 | $10.00 |
Findings by check
EBS-001 · Unattached EBS volumes (4)
Volumes in the 'available' state are billed every month but serve nothing - often left behind by deleted instances.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
vol-4d2b74392f2aca509 | 100 GiB io1, 3,000 provisioned IOPS (perf-test), not attached to any instance | Confirm with the owner, snapshot it (cheap insurance), then delete the volume. The command waits for the snapshot to finish (up to 10 minutes); if the wait times out, delete the volume once the snapshot shows 'completed'. Delete the snapshot too after a month or two if nobody needs it.Command (risk: medium)SNAP=$(aws ec2 create-snapshot --region ap-south-1 --volume-id vol-4d2b74392f2aca509 --description 'billdoctor: backup before delete' --query SnapshotId --output text) && aws ec2 wait snapshot-completed --region ap-south-1 --snapshot-ids "$SNAP" && aws ec2 delete-volume --region ap-south-1 --volume-id vol-4d2b74392f2aca509 |
ap-south-1 | $208.10 | $208.10 |
vol-e8b333159767c43f1 | 1000 GiB gp2 (forgotten-ml-dataset), not attached to any instance | Confirm with the owner, snapshot it (cheap insurance), then delete the volume. The command waits for the snapshot to finish (up to 10 minutes); if the wait times out, delete the volume once the snapshot shows 'completed'. Delete the snapshot too after a month or two if nobody needs it.Command (risk: medium)SNAP=$(aws ec2 create-snapshot --region us-east-1 --volume-id vol-e8b333159767c43f1 --description 'billdoctor: backup before delete' --query SnapshotId --output text) && aws ec2 wait snapshot-completed --region us-east-1 --snapshot-ids "$SNAP" && aws ec2 delete-volume --region us-east-1 --volume-id vol-e8b333159767c43f1 |
us-east-1 | $100.00 | $100.00 |
vol-18086aeeb8ae02f33 | 500 GiB gp3 (migration-temp), not attached to any instance | Confirm with the owner, snapshot it (cheap insurance), then delete the volume. The command waits for the snapshot to finish (up to 10 minutes); if the wait times out, delete the volume once the snapshot shows 'completed'. Delete the snapshot too after a month or two if nobody needs it.Command (risk: medium)SNAP=$(aws ec2 create-snapshot --region ap-south-1 --volume-id vol-18086aeeb8ae02f33 --description 'billdoctor: backup before delete' --query SnapshotId --output text) && aws ec2 wait snapshot-completed --region ap-south-1 --snapshot-ids "$SNAP" && aws ec2 delete-volume --region ap-south-1 --volume-id vol-18086aeeb8ae02f33 |
ap-south-1 | $45.60 | $45.60 |
vol-2173027d8b7ee1d33 | 200 GiB gp2 (old-jenkins-data), not attached to any instance | Confirm with the owner, snapshot it (cheap insurance), then delete the volume. The command waits for the snapshot to finish (up to 10 minutes); if the wait times out, delete the volume once the snapshot shows 'completed'. Delete the snapshot too after a month or two if nobody needs it.Command (risk: medium)SNAP=$(aws ec2 create-snapshot --region ap-south-1 --volume-id vol-2173027d8b7ee1d33 --description 'billdoctor: backup before delete' --query SnapshotId --output text) && aws ec2 wait snapshot-completed --region ap-south-1 --snapshot-ids "$SNAP" && aws ec2 delete-volume --region ap-south-1 --volume-id vol-2173027d8b7ee1d33 |
ap-south-1 | $22.80 | $22.80 |
EBS-002 · gp2 volumes that should be gp3 (1)
gp3 is about 20% cheaper than gp2 per GiB and has a 3,000 IOPS baseline regardless of size. The change is online.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
vol-62d334940312f5472 | 100 GiB gp2 attached to i-bd540e2a038f46d43 | Modify the volume to gp3 (no downtime).Command (risk: low)aws ec2 modify-volume --region ap-south-1 --volume-id vol-62d334940312f5472 --volume-type gp3 |
ap-south-1 | $11.40 | $2.28 |
EBS-003 · Old EBS snapshots (2)
Snapshots older than the retention you need, and not used by an AMI, keep costing storage forever.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
snap-8f13d4c9317c14b7e | 410 days old, from a 200 GiB volume. pre-migration backup 2025 | Keep only what your retention policy needs (e.g. 30-90 days) - set up Amazon Data Lifecycle Manager instead of manual snapshots. Saving shown is an upper bound (snapshots are incremental).Command (risk: medium)aws ec2 delete-snapshot --region ap-south-1 --snapshot-id snap-8f13d4c9317c14b7e |
ap-south-1 | $10.00 | $10.00 |
snap-5775104f2ff9157e7 | 220 days old, from a 200 GiB volume. manual backup before upgrade | Keep only what your retention policy needs (e.g. 30-90 days) - set up Amazon Data Lifecycle Manager instead of manual snapshots. Saving shown is an upper bound (snapshots are incremental).Command (risk: medium)aws ec2 delete-snapshot --region ap-south-1 --snapshot-id snap-5775104f2ff9157e7 |
ap-south-1 | $10.00 | $10.00 |
EC2-001 · Unassociated Elastic IPs (2)
Since February 2024 every public IPv4 address costs $0.005/hour; an Elastic IP attached to nothing is pure waste.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
eipalloc-4ffa7c394b6d7eebe | 127.37.226.155 is not attached to anything | Release it unless an allow-list somewhere depends on this exact IP.Command (risk: medium)aws ec2 release-address --region ap-south-1 --allocation-id eipalloc-4ffa7c394b6d7eebe |
ap-south-1 | $3.65 | $3.65 |
eipalloc-f6f25954c79c40702 | 127.172.44.47 is not attached to anything | Release it unless an allow-list somewhere depends on this exact IP.Command (risk: medium)aws ec2 release-address --region us-east-1 --allocation-id eipalloc-f6f25954c79c40702 |
us-east-1 | $3.65 | $3.65 |
EC2-002 · Long-stopped instances still paying for disks (1)
A stopped instance costs nothing for compute, but its EBS volumes are billed in full.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
i-1a1444233b43272f1 | t3.large (old-build-agent) stopped for 75 days, 1 volume(s) attached | If it is not coming back: create an AMI (keeps a restorable copy), then terminate the instance. Volumes with 'delete on termination' turned off survive the termination; delete them too (the AMI holds a copy) or they show up as unattached volumes next time.Command (risk: high)IMG=$(aws ec2 create-image --region ap-south-1 --instance-id i-1a1444233b43272f1 --name billdoctor-i-1a1444233b43272f1 --query ImageId --output text) && aws ec2 wait image-available --region ap-south-1 --image-ids "$IMG" && aws ec2 terminate-instances --region ap-south-1 --instance-ids i-1a1444233b43272f1 |
ap-south-1 | $13.68 | $13.68 |
EC2-003 · Idle running instances (1)
Instances averaging under 5% CPU (and never above 20%) for two weeks are usually forgotten or oversized.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
i-bd540e2a038f46d43 | t2.large (legacy-reporting): average CPU 2.7%, peak 7.5% over 13 days | Confirm with the owner. Then stop it, schedule it (office hours only), or downsize. Saving shown assumes it can be stopped.Command (risk: high)aws ec2 stop-instances --region ap-south-1 --instance-ids i-bd540e2a038f46d43 |
ap-south-1 | $72.42 | $72.42 |
EC2-004 · Previous-generation instance types (2)
Older families (t2, m4, c4, r4...) cost more for less performance than their current replacements.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
i-bd540e2a038f46d43 | t2.large -> t3.large | Change the instance type to t3.large during a maintenance window (stop, change type, start). Newer types run on Nitro: very old AMIs need the ENA and NVMe drivers (current images have them). A stop/start changes the public IP unless it is an Elastic IP. Graviton (t4g/m7g) is cheaper still if the software runs on ARM.Command (risk: medium)aws ec2 stop-instances --region ap-south-1 --instance-ids i-bd540e2a038f46d43 && aws ec2 wait instance-stopped --region ap-south-1 --instance-ids i-bd540e2a038f46d43 && aws ec2 modify-instance-attribute --region ap-south-1 --instance-id i-bd540e2a038f46d43 --instance-type Value=t3.large && aws ec2 start-instances --region ap-south-1 --instance-ids i-bd540e2a038f46d43 |
ap-south-1 | $72.42 | $7.01 |
i-9f757bf59aaf5d499 | m4.large -> m6i.large | Change the instance type to m6i.large during a maintenance window (stop, change type, start). Newer types run on Nitro: very old AMIs need the ENA and NVMe drivers (current images have them). A stop/start changes the public IP unless it is an Elastic IP. Graviton (t4g/m7g) is cheaper still if the software runs on ARM.Command (risk: medium)aws ec2 stop-instances --region ap-south-1 --instance-ids i-9f757bf59aaf5d499 && aws ec2 wait instance-stopped --region ap-south-1 --instance-ids i-9f757bf59aaf5d499 && aws ec2 modify-instance-attribute --region ap-south-1 --instance-id i-9f757bf59aaf5d499 --instance-type Value=m6i.large && aws ec2 start-instances --region ap-south-1 --instance-ids i-9f757bf59aaf5d499 |
ap-south-1 | $73.00 | $2.92 |
ELB-001 · Load balancers with no targets (1)
A load balancer is billed by the hour even when nothing is registered behind it.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
old-promo-alb | application load balancer with 1 target group(s) and 0 registered targets | Delete it if the service is gone. First check DNS records pointing at it, and its listener rules: a load balancer without targets can still serve redirects or fixed responses.Command (risk: medium)aws elbv2 delete-load-balancer --region ap-south-1 --load-balancer-arn arn:aws:elasticloadbalancing:ap-south-1:123456789012:loadbalancer/app/old-promo-alb/679b32dd68cc9392 |
ap-south-1 | $16.43 | $16.43 |
LOGS-001 · Log groups that never expire (2)
The default retention is 'never expire', so log storage grows forever.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
/aws/lambda/old-report-fn | 12.00 GB stored, retention: never expire | Set a retention period that matches your needs (e.g. 30 days for app logs, longer for audit logs). Saving depends on how old the data is, so it is not estimated.Command (risk: low)aws logs put-retention-policy --region ap-south-1 --log-group-name '/aws/lambda/old-report-fn' --retention-in-days 30 |
ap-south-1 | $0.36 | - |
/ecs/web | 48.00 GB stored, retention: never expire | Set a retention period that matches your needs (e.g. 30 days for app logs, longer for audit logs). Saving depends on how old the data is, so it is not estimated.Command (risk: low)aws logs put-retention-policy --region ap-south-1 --log-group-name '/ecs/web' --retention-in-days 30 |
ap-south-1 | $1.44 | - |
NAT-001 · Idle NAT gateways (1)
A NAT gateway costs ~$33-41/month before any data. Many dev VPCs keep one for traffic that never comes.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
nat-fd7db26a016ea87b1 | moved 0.51 GB (in + out) in 14 days (VPC vpc-663763701c56d4e86) | Delete it if private subnets do not need the internet; use VPC endpoints for S3/DynamoDB/ECR, or public subnets with tight security groups for small dev setups.Command (risk: high)aws ec2 delete-nat-gateway --region ap-south-1 --nat-gateway-id nat-fd7db26a016ea87b1 |
ap-south-1 | $40.88 | $40.88 |
RDS-001 · Idle RDS databases (1)
A database with zero connections for two weeks is usually a forgotten test or migration copy.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
analytics-copy | db.t3.medium postgres 100 GiB: 0 connections in 13 days | Take a final snapshot and delete it (stopping only lasts 7 days before AWS restarts it).Command (risk: high)aws rds delete-db-instance --region ap-south-1 --db-instance-identifier analytics-copy --final-db-snapshot-identifier analytics-copy-billdoctor-final |
ap-south-1 | $63.96 | $63.96 |
S3-001 · S3 buckets without lifecycle rules (2)
Without lifecycle rules, incomplete multipart uploads and old object versions are kept (and billed) forever.
| Resource | Detail | Recommendation | Region | Now / month | Saving |
|---|---|---|---|---|---|
acme-uploads-demo | incomplete multipart uploads are never cleaned up; versioning is on but old versions never expire | Add a lifecycle rule: abort incomplete multipart uploads after 7 days and expire noncurrent versions after 30-90 days.Command (risk: low)aws s3api put-bucket-lifecycle-configuration --region ap-south-1 --bucket acme-uploads-demo --lifecycle-configuration '{"Rules":[{"ID":"billdoctor-hygiene","Status":"Enabled","Filter":{},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7},"NoncurrentVersionExpiration":{"NoncurrentDays":60}}]}' # WARNING: replaces existing rules - merge them first |
ap-south-1 | - | - |
acme-exports-demo | incomplete multipart uploads are never cleaned up | Add a lifecycle rule: abort incomplete multipart uploads after 7 days.Command (risk: low)aws s3api put-bucket-lifecycle-configuration --region ap-south-1 --bucket acme-exports-demo --lifecycle-configuration '{"Rules":[{"ID":"billdoctor-hygiene","Status":"Enabled","Filter":{},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}' # WARNING: replaces existing rules - merge them first |
ap-south-1 | - | - |
Method and assumptions
- Only read-only API calls were made (describe/list/get and CloudWatch metrics over the last 14 days).
- Prices are approximate on-demand list prices (2026-10); discounts, Savings Plans and Reserved Instances are not applied.
- Savings are estimates. Confirm with the resource owner, and snapshot or back up before deleting anything.
- ap-south-1: no local price for ebs_iops_month/io1; used us-east-1 as an approximation
- ap-south-1: no local price for ebs_iops_month/gp3; used us-east-1 as an approximation
- ap-south-1: no local price for ec2_hour/m4.large; used us-east-1 as an approximation
- ap-south-1: no local price for ec2_hour/m6i.large; used us-east-1 as an approximation
- ap-south-1: no local price for alb_hour; used us-east-1 as an approximation
- ap-south-1: no local price for logs_gb_month; used us-east-1 as an approximation
- ap-south-1: no local price for rds_hour/db.t3.medium; used us-east-1 as an approximation