AWS CLI Command
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=stopped" \
--query 'Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,Stopped:StateTransitionReason}' \
--output table
Fix Options
- Terminate if confirmed unused
- Snapshot EBS then terminate
- Move to Savings Plans if intermittently needed
- Use ServerScheduler to auto-stop on a schedule instead
AWS CLI Command
aws ec2 describe-instances \
--filters "Name=tag:Environment,Values=dev,staging,qa,test" \
--query 'Reservations[].Instances[].{ID:InstanceId,State:State.Name,Type:InstanceType}' \
--output table
Fix Options
- Schedule stop at 6pm, start at 8am MonβFri
- Add weekend shutdown
- ServerScheduler: visual grid scheduler, one-click setup for all envs
AWS CLI Command
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--period 2592000 \
--statistics Average \
--dimensions Name=InstanceId,Value=i-xxxx \
--start-time 2024-01-01T00:00:00Z \
--end-time 2024-01-31T00:00:00Z
Fix Options
- Downsize to next smaller instance type
- Consolidate multiple under-used instances
- Use AWS Compute Optimizer recommendations
- Enable auto-scaling and remove static over-allocation
AWS CLI Command
aws ec2 describe-instances \
--query 'Reservations[].Instances[?starts_with(InstanceType,`m3`) || starts_with(InstanceType,`c3`) || starts_with(InstanceType,`t2`)].{ID:InstanceId,Type:InstanceType}' \
--output table
Fix Options
- Change instance type in the console (requires stop/start)
- t2 β t3 is almost always a drop-in replacement
- m3/c3 β m5/c5 requires testing but is straightforward
Where to Check
AWS Cost Explorer β Savings Plans β Coverage Report. Filter by service EC2. Any coverage <80% represents uncovered on-demand spend.
Fix Options
- Compute Savings Plan: most flexible, covers EC2/Fargate/Lambda
- EC2 Instance Savings Plan: deeper discount for specific families
- Start with 1-year no-upfront to minimise commitment risk
AWS CLI Command
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocationId:AllocationId}' \
--output table
Fix
Release any unattached Elastic IPs you don't need. Note: releasing is permanent β if you need to reserve the IP, associate it with a running instance first.
AWS CLI Command
aws elbv2 describe-load-balancers --output table
# Then for each LB:
aws elbv2 describe-target-groups \
--load-balancer-arn \
--query 'TargetGroups[].TargetGroupArn'
Fix
Delete LBs with no target groups or only empty target groups. Check CloudWatch metrics for RequestCount = 0 over the last 14 days before deleting.
Where to Check
EC2 β Auto Scaling Groups. Compare MinSize vs actual in-service count vs CloudWatch CPU metrics. If actual rarely scales above MinSize, MinSize is too high.
Fix
- Reduce MinSize to 1 (or 0 for dev/staging with scheduled scaling)
- Add scheduled scale-in for overnight/weekend periods
- Enable target tracking based on CPU/request count
Where to Check
AWS Pricing page β compare instance pricing across regions. Use Infracost or CloudPrice to diff costs. eu-west-1 is often cheaper than us-east-1 for equivalent types.
Fix
Snapshot β copy AMI to target region β launch from AMI. Also check data transfer costs before migrating β regional egress can offset compute savings.
Common Patterns
- Jenkins/GitLab runner: online 24/7, builds run 2β4h/day
- Report generator: scheduled nightly, runs 30 min
- Data pipeline: weekday batches, idle all weekend
Fix Options
- Schedule start/stop around the job window (+ 30min buffer)
- Move to Fargate/Lambda for truly event-driven jobs
- ServerScheduler: schedule precise on/off times visually
AWS CLI Command
aws rds describe-db-instances \
--query 'DBInstances[?TagList[?Key==`Environment` && (Value==`dev` || Value==`staging`)]].{ID:DBInstanceIdentifier,Class:DBInstanceClass,Status:DBInstanceStatus}' \
--output table
Fix Options
- Manual stop in console (auto-restarts after 7 days β need automation)
- ServerScheduler handles RDS stop/start on schedule natively
- Aurora Serverless v2: scales to 0 when idle (good for intermittent dev DBs)
AWS CLI Command
aws rds describe-db-instances \
--query 'DBInstances[?MultiAZ==`true`].{ID:DBInstanceIdentifier,MultiAZ:MultiAZ,Class:DBInstanceClass}' \
--output table
Fix
Modify the instance β disable Multi-AZ. Requires a brief maintenance window. Only disable for environments where downtime during maintenance is acceptable (dev/staging).
AWS CLI Command
aws rds describe-db-instances \
--query 'DBInstances[].{ID:DBInstanceIdentifier,Storage:AllocatedStorage,Type:StorageType}' \
--output table
Fix
- Migrate gp2 β gp3: same performance, 30% cheaper, no downtime
- RDS doesn't support storage reduction β prevents over-allocation next time
AWS CLI Command
aws rds describe-db-snapshots \
--snapshot-type manual \
--query 'DBSnapshots[].{ID:DBSnapshotIdentifier,Created:SnapshotCreateTime,Size:AllocatedStorage}' \
--output table
Fix
Delete snapshots older than your retention policy. Set a lifecycle policy going forward. AWS Backup can automate expiry and compliance.
Where to Check
CloudWatch β RDS β DatabaseConnections metric for your replica. If it's consistently near 0 while the primary is active, the replica is receiving no queries.
Fix
- Delete the replica if it was created "just in case"
- If kept for failover: verify promotion readiness instead of keeping always-on
- Route reads explicitly if you want to justify the cost
AWS CLI Command
aws rds describe-db-instances \
--query 'DBInstances[].{ID:DBInstanceIdentifier,BackupRetention:BackupRetentionPeriod}' \
--output table
Fix
Reduce backup retention for dev/staging to 1β3 days. For production, 7 days is usually sufficient unless compliance requires more.
Where to Check
RDS β Reserved Instances β check coverage. Cost Explorer β Reserved Instance Coverage β filter to RDS service.
Fix
Purchase 1-year partial upfront RI for your primary production DB class. Multi-AZ RI discounts apply to the primary only; standby is charged on-demand.
AWS CLI Command
aws rds describe-db-clusters \
--query 'DBClusters[].{ID:DBClusterIdentifier,Status:Status,Engine:Engine}' \
--output table
Fix
- Delete unused Aurora clusters (snapshot first)
- For intermittent dev use, switch to Aurora Serverless v2 (scales to 0)
AWS CLI Command
aws ec2 describe-volumes \
--filters "Name=status,Values=available" \
--query 'Volumes[].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}' \
--output table
Fix
Snapshot then delete unrecognised volumes. If you need the data, keep the snapshot (much cheaper at $0.05/GB/mo). Delete the volume.
AWS CLI Command
aws ec2 describe-volumes \
--filters "Name=volume-type,Values=gp2" \
--query 'Volumes[].{ID:VolumeId,Size:Size,IOPS:Iops}' \
--output table
Fix
Modify volume type from gp2 β gp3 in the console or via CLI. No downtime required. Takes a few minutes. AWS does this live on the running volume.
AWS CLI Command
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?StartTime<=`2024-01-01`].{ID:SnapshotId,Size:VolumeSize,Date:StartTime}' \
--output table
Fix
- Create a Data Lifecycle Manager (DLM) policy to auto-expire old snapshots
- Delete snapshots older than your defined retention period
Where to Check
CloudWatch β EBS β VolumeReadOps + VolumeWriteOps. Compare peak actual IOPS over 30 days to provisioned IOPS. If peak < 50% of provisioned, reduce.
Fix
Reduce provisioned IOPS to 120β130% of your peak actual usage. Or migrate to gp3 if IOPS are under 16,000 β gp3 includes 3,000 IOPS free and can add up to 16,000.
Where to Check
S3 β Metrics β Storage Lens. Enable S3 Intelligent-Tiering for buckets with mixed access patterns. AWS moves objects automatically between tiers based on actual access.
Fix
Create S3 Lifecycle rules to transition objects to S3-IA after 30 days, Glacier Instant after 90 days. Or use Intelligent-Tiering for uncertain access patterns.
AWS CLI Command
aws s3api get-bucket-versioning --bucket my-bucket
# List non-current versions:
aws s3api list-object-versions --bucket my-bucket \
--query 'DeleteMarkers[].{Key:Key,VersionId:VersionId}'
Fix
Add lifecycle rule: expire non-current versions after 30 days. Permanently delete delete markers when all versions are gone. Run a one-time cleanup for historical accumulation.
Where to Check
CloudWatch β NatGateway β BytesOutToDestination. Cost Explorer β filter by service "EC2-Other" β usage type contains "NatGateway". High costs often from EC2βS3 routing through NAT.
Fix
- Use S3 Gateway Endpoint (free) for EC2βS3 traffic to bypass NAT
- Use Interface Endpoints for other AWS services in VPC
- Cache ECR images locally if Docker pull is the source
How to Check
AWS Compute Optimizer β Lambda recommendations. Also check CloudWatch max memory used for each function. Target: allocated = max_used Γ 1.3 (30% headroom).
Fix
Reduce memory to 130% of max observed. Use AWS Lambda Power Tuning (open-source tool) to find the cost-optimal memory size automatically for each function.
Where to Check
CloudWatch β Lambda β ProvisionedConcurrencyUtilization. If average < 60%, consider reducing provisioned concurrency or using auto-scaling for provisioned concurrency instead.
Fix
- Reduce provisioned concurrency count
- Use Application Auto Scaling to scale provisioned concurrency on a schedule
- Consider Lambda SnapStart for Java functions (built-in cold start fix, no cost)
AWS CLI Command
aws logs describe-log-groups \
--query 'logGroups[?retentionInDays==null].{Name:logGroupName,Size:storedBytes}' \
--output table
Fix
Set retention to 30 days for dev, 90 days for prod (or per compliance). Use a one-line CLI loop to set retention on all log groups in one go.
Where to Check
SQS β Dead Letter Queue β NumberOfMessagesSent metric. Lambda β Throttles and Error metrics for associated functions. Any DLQ with growing message count needs investigation.
Fix
Purge the DLQ once the root cause is fixed. Set maxReceiveCount on the main queue to limit retries (3β5 is typical). Add DLQ alarms to catch this going forward.
AWS CLI Command
aws events list-rules \
--query 'Rules[?ScheduleExpression!=null && State==`ENABLED`].{Name:Name,Schedule:ScheduleExpression}' \
--output table
Fix
Disable or delete rules for functions that no longer exist or serve a purpose. Check the function's invocation count in CloudWatch to confirm it's actually being triggered.
Where to Check
Cost Explorer β filter by usage type β contains "DataTransfer-Regional". Group by Usage Type. Enable VPC Flow Logs + Athena queries to identify top talker pairs across AZs.
Fix
- Pin services to a single AZ where cross-AZ HA isn't needed
- Use AZ-aware load balancing to prefer same-AZ targets
- Add same-AZ routing to service mesh configs (Istio, App Mesh)
AWS CLI Command
aws ec2 describe-vpc-endpoints \
--query 'VpcEndpoints[].{Type:VpcEndpointType,Service:ServiceName,State:State}' \
--output table
Fix
Create S3 and DynamoDB Gateway Endpoints for every VPC with private subnets. Free. Takes 2 minutes. Add route table entries for private subnets to route S3/DynamoDB through the endpoint.
Where to Check
CloudFront β Distributions list β check each for Requests metric in CloudWatch. Any distribution with near-zero requests for 30+ days should be reviewed for deletion.
Fix
Disable (not delete) first to confirm nothing breaks. Then delete after 24β48 hours. Keep the SSL certificate if the domain is still in use elsewhere.
AWS CLI Command
aws ec2 describe-vpcs \
--query 'Vpcs[].{ID:VpcId,CIDR:CidrBlock,IsDefault:IsDefault,Tags:Tags}' \
--output table
# Then check for NAT Gateways in each:
aws ec2 describe-nat-gateways --filter "Name=state,Values=available"
Fix
Delete orphaned NAT Gateways first (large cost). Then release Elastic IPs associated with them. Then clean up subnets, route tables, IGWs, and finally the VPC itself.
Where to Set Up
AWS Cost Explorer β Cost Anomaly Detection β Create Monitor. Set alert threshold (e.g. $20 unexpected spend). Add SNS notification to email or Slack.
Fix
Create monitors per service (EC2, RDS, etc.) with individual thresholds. This is the most impactful prevention tool β it catches new idle resources within hours, not at month-end.
AWS CLI Command
aws elasticache describe-cache-clusters \
--query 'CacheClusters[].{ID:CacheClusterId,Status:CacheClusterStatus,Type:CacheNodeType}' \
--output table
Fix Options
- ServerScheduler can automate ElastiCache cluster deletion + recreation on a schedule
- For dev: delete cluster at end of day, recreate in the morning (cache is ephemeral anyway)
- Move to Valkey (ElastiCache open-source compatible, often cheaper at smaller sizes)
Where to Check
CloudWatch β ElastiCache β DatabaseMemoryUsagePercentage (Redis) or FreeableMemory. If memory usage is consistently <50%, consider downgrading one node size.
Fix
Modify cluster node type (requires brief failover for cluster mode). Scale down one size at a time, monitor for evictions (CurrEvictions metric) after change.
AWS CLI Command
# List all alarms (check for INSUFFICIENT_DATA = orphaned)
aws cloudwatch describe-alarms \
--state-value INSUFFICIENT_DATA \
--query 'MetricAlarms[].{Name:AlarmName,Reason:StateReason}' \
--output table
Fix
Delete INSUFFICIENT_DATA alarms pointing at non-existent resources. Audit dashboards for last-opened date. Delete unused dashboards (AWS doesn't show last-opened β sort by name for old project names).
Where to Set Up
Billing β Budgets β Create Budget β Monthly Cost Budget. Set two thresholds: 80% actual + 100% forecast. Email or SNS alert. Free for first 2 budgets per account.
Pro Tip
Also set a service-level budget for EC2 and RDS separately β these are usually the biggest line items and worth monitoring independently. $3/month each after the free tier.
Where to Check
AWS Console β Trusted Advisor β Cost Optimization category. On Basic plan: only 6 checks available. On Business+: full 115 checks including idle EC2, unassociated EIPs, underutilised RDS.
Free Alternative
AWS Compute Optimizer (free) covers EC2, Lambda, ECS, EBS. Cost Explorer Rightsizing Recommendations covers EC2 with no Trusted Advisor required.
Stop paying for idle AWS resources
The quickest fix for dev/staging compute and database waste is automated scheduling. ServerScheduler shuts down EC2, RDS, and ElastiCache on a visual weekly grid β no code, no EventBridge config, no Lambda to maintain.
Try ServerScheduler Free β