aws-cost-saver
AWS cost optimization scanner with Compute Optimizer ML integration, data transfer analysis, and 173 checks. Use when scanning AWS accounts or analyzing domains (compute, storage, database, networking, serverless, reservations, containers, advanced_databases, analytics, data_pipelines, storage_advanced).
AWS Cost Optimization Scanner
Scan ONE domain for cost optimization findings.
Input
domain: compute | storage | database | networking | serverless | reservations | containers | advanced_databases | analytics | data_pipelines | storage_advancedregion: AWS region to scancompliance: [HIPAA, SOC2, PCI-DSS] or emptyprofile: AWS profile name
11 Domains (173 checks total)
| Domain | Checks | Resources |
|---|---|---|
| compute | 27 | EC2, EBS, AMIs, snapshots, EIPs, Compute Optimizer |
| storage | 24 | S3, EFS, CloudWatch Logs, CloudTrail, Secrets Manager |
| database | 15 | RDS, DynamoDB, ElastiCache |
| networking | 18 | NAT, ELB, VPC endpoints, data transfer, Route 53 |
| serverless | 10 | Lambda, API Gateway, SQS, Step Functions |
| reservations | 12 | RI coverage, Savings Plans, purchase recommendations |
| containers | 16 | ECS, EKS, Fargate, ECR |
| advanced_databases | 18 | Aurora, DocumentDB, Neptune, Redshift |
| analytics | 15 | SageMaker, EMR, OpenSearch, QuickSight |
| data_pipelines | 12 | Kinesis, MSK, Glue, EventBridge |
| storage_advanced | 6 | FSx, AWS Backup |
Workflow
- Read
checks/all_checks.yamlfor domain checks - Run AWS CLI commands via MCP tool
- Save resource inventory
- Compare against thresholds
- Return findings + resources
Compliance Rules
| Compliance | Rule | skip_reason |
|---|---|---|
| HIPAA | Skip phi=true tags, healthcare names | hipaa-protected |
| SOC2 | Don't delete logs (but CAN set retention) | soc2-audit-required |
| PCI-DSS | Skip pci=true tags, payment VPCs | pci-dss-protected |
Tag-Based Exclusions
SkipCostOpt Tag
Resources with SkipCostOpt=true are excluded from ALL checks.
Exclusion Tags (Honor Any)
| Tag Key | Truthy Values |
|---|---|
| SkipCostOpt | true, 1, yes |
| CostOptExclude | true, 1, yes |
| DoNotOptimize | true, 1, yes |
Check Tags in Resource Data
# Tags included in describe-* responses
# Check Tags array for exclusion keys
Output When Excluded
{
"resource_id": "i-abc123",
"status": "excluded",
"exclusion_reason": "Tag SkipCostOpt=true"
}
Output Format
{
"domain": "compute",
"region": "us-east-1",
"scanned_at": "2026-01-19T15:00:00Z",
"resources": {
"ec2_instances": [...],
"ebs_volumes": [...]
},
"resource_summary": {
"total_resources": 17,
"total_findings": 3
},
"findings": [
{
"check_id": "EC2-001",
"resource_id": "i-abc123",
"resource_name": "web-server-1",
"title": "Idle EC2 Instance",
"domain": "compute",
"severity": "high",
"category": "idle",
"monthly_savings": 70.00,
"confidence": 85,
"description": "CPU < 5% for 14+ days",
"recommendation": "Terminate or stop",
"environment": "production",
"skip_reason": null,
"actionable": true,
"details": {...}
}
]
}
Resource Inventory by Domain
compute
ec2_instances: All instancesebs_volumes: All volumeselastic_ips: All EIPssnapshots: Account snapshotsamis: Account AMIscompute_optimizer_status: Enrollment status (Active/Inactive)compute_optimizer_recommendations: CO recommendations (if enrolled)
storage
s3_buckets: All bucketsefs_filesystems: All EFScloudwatch_log_groups: All log groups with retentioncloudtrail_trails: All trailssecrets: Secrets Manager secrets (with last accessed date)
database
rds_instances: All RDSrds_snapshots: Manual snapshotsdynamodb_tables: All tableselasticache_clusters: All clusters
networking
nat_gateways: All NAT gatewaysload_balancers: ALBs/NLBsvpc_endpoints: All endpointsdata_transfer_costs: USAGE_TYPE breakdown from Cost Explorerroute53_zones: Hosted zones with record counts
serverless
lambda_functions: All functionsapi_gateways: REST/HTTP APIssqs_queues: All queuesstep_functions: State machines
reservations
reserved_instances: EC2 RIsreserved_db_instances: RDS RIssavings_plans: Active plansri_purchase_recommendations: AWS-generated RI recommendationssp_purchase_recommendations: AWS-generated SP recommendations
containers
ecs_clusters: All ECS clustersecs_services: Services per clusterecs_task_definitions: Task definitionseks_clusters: All EKS clusterseks_nodegroups: Node groups per clusterfargate_tasks: Fargate tasksecr_repositories: ECR repos with lifecycle policy status
advanced_databases
aurora_clusters: Aurora DB clustersaurora_instances: Aurora instancesdocumentdb_clusters: DocumentDB clustersneptune_clusters: Neptune clustersredshift_clusters: Redshift clusters
analytics
sagemaker_notebooks: Notebook instancessagemaker_endpoints: Inference endpointsemr_clusters: Active EMR clustersopensearch_domains: OpenSearch domainsquicksight_datasets: QuickSight datasets
data_pipelines
kinesis_streams: Data streamsfirehose_streams: Firehose delivery streamsmsk_clusters: MSK Kafka clustersglue_jobs: Glue ETL jobseventbridge_rules: EventBridge rules
storage_advanced
fsx_filesystems: FSx file systemsbackup_plans: AWS Backup plansbackup_vaults: Backup vaults
Cost-Tiered Confidence
Higher-cost findings need MORE evidence before flagging.
Confidence Tiers
| Tier | Monthly Cost | Min Age | Min Confidence | Signals Required |
|---|---|---|---|---|
| LOW | < $20 | 1 day | 65% | 1 signal OK |
| MEDIUM | $20 - $100 | 3 days | 75% | 2+ signals |
| HIGH | > $100 | 7 days | 85% | 2+ signals |
Apply Before Reporting
- Calculate monthly_savings → determine tier
- Check resource age >= tier.min_age_days
- Check confidence >= tier.min_confidence
- For MEDIUM/HIGH: verify 2+ signals agree
- If any check fails → filter out finding
Quick Adjustments (Apply After Tier Check)
- -30% if resource < 7 days old
- -10% for production environment
- +10% for dev/test environment
- -30% if part of Auto Scaling Group
Multi-Signal Idle Detection
CRITICAL: Do NOT flag resources as idle based on CPU alone. Use weighted multi-signal scoring.
EC2 Idle Detection
| Signal | Metric | Threshold | Weight |
|---|---|---|---|
| CPU | CPUUtilization avg | < 5% | 0.40 |
| Network In | NetworkIn | < 0.1 GB/day | 0.15 |
| Network Out | NetworkOut | < 0.1 GB/day | 0.15 |
| Connections | NetworkPacketsIn | 0 | 0.20 |
| Disk I/O | DiskReadOps+WriteOps | < 10/sec | 0.10 |
Idle Score Formula:
idleScore = sum(weights where threshold met)
Threshold: idleScore >= 0.60 required to flag as idle
Required AWS Commands
# Collect ALL signals (not just CPU)
aws cloudwatch get-metric-statistics --namespace AWS/EC2 \
--metric-name CPUUtilization --dimensions Name=InstanceId,Value={id} \
--start-time {14d_ago} --end-time {now} --period 86400 --statistics Average Maximum
aws cloudwatch get-metric-statistics --namespace AWS/EC2 \
--metric-name NetworkIn --dimensions Name=InstanceId,Value={id} \
--start-time {14d_ago} --end-time {now} --period 86400 --statistics Sum
aws cloudwatch get-metric-statistics --namespace AWS/EC2 \
--metric-name NetworkPacketsIn --dimensions Name=InstanceId,Value={id} \
--start-time {14d_ago} --end-time {now} --period 86400 --statistics Sum
Finding Output Format
{
"check_id": "EC2-001",
"idle_detection": {
"signals": {
"cpu_avg": {"value": 2.1, "threshold": 5, "passed": true, "weight": 0.40},
"network_in_gb": {"value": 0.05, "threshold": 0.1, "passed": true, "weight": 0.15},
"connections": {"value": 145, "threshold": 0, "passed": false, "weight": 0.20}
},
"idle_score": 0.55,
"threshold": 0.60,
"result": "NOT_IDLE - has active connections"
}
}
AWS Compute Optimizer Integration (FREE)
Use Compute Optimizer as PRIMARY signal for rightsizing and idle detection. Falls back to CloudWatch thresholds if CO is not enabled.
Step 1: Check Enrollment
aws compute-optimizer get-enrollment-status
If status is Active, use CO recommendations. If Inactive, fall back to manual CloudWatch analysis.
Step 2: ML-Powered Rightsizing (EC2-024)
aws compute-optimizer get-ec2-instance-recommendations
Returns recommendations with:
finding: UNDER_PROVISIONED, OVER_PROVISIONED, OPTIMIZED, or IDLErecommendationOptions: Specific instance types with projected performance- Accounts for CPU, memory, network, and disk (not just CPU)
Use CO finding directly instead of manual threshold checks when available.
Step 3: Idle Detection (EC2-026)
aws compute-optimizer get-ec2-instance-recommendations \
--filters name=Finding,values=Idle
This is more accurate than manual idle detection because it uses ML trained on usage patterns.
Fallback (CO Not Enabled)
If Compute Optimizer is not active, use the manual multi-signal idle detection below.
Memory Metric Graceful Degradation (EC2-027)
Check if CloudWatch Agent memory metrics exist. If available, use them to improve idle detection accuracy. If not, proceed without penalizing confidence.
Check Availability
aws cloudwatch list-metrics --namespace CWAgent --metric-name mem_used_percent \
--dimensions Name=InstanceId,Value={instance_id}
If Available
Add memory as a signal in idle detection:
| Signal | Metric | Threshold | Weight |
|---|---|---|---|
| Memory | mem_used_percent | < 10% | 0.15 |
Redistribute weights: CPU 0.35, Network In 0.10, Network Out 0.10, Connections 0.15, Disk I/O 0.10, Memory 0.15
CRITICAL: An instance at <5% CPU but >70% memory is likely a cache server — do NOT flag as idle.
If Not Available
- Note:
"memory_data": "unavailable - CloudWatch Agent not installed" - Do NOT reduce confidence due to missing memory data
- Proceed with standard 5-signal detection
Data Transfer Cost Analysis (NET-016, NET-017)
Most tools miss data transfer costs. Use USAGE_TYPE grouping to surface hidden charges.
Query Data Transfer Breakdown
aws ce get-cost-and-usage \
--time-period Start={30_days_ago},End={now} \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon Elastic Compute Cloud - Compute"]}}'
Key Usage Types to Flag
| Usage Type | What It Is | Cost |
|---|---|---|
DataTransfer-Regional-Bytes | Cross-AZ traffic | $0.01/GB each direction |
NatGateway-Bytes | NAT data processing | $0.045/GB |
DataTransfer-Out-Bytes | Internet egress | $0.09/GB first 10TB |
DataTransfer-In-Bytes | Internet ingress | Free |
Findings Format
{
"check_id": "NET-017",
"title": "High Data Transfer Costs",
"monthly_savings": 45.00,
"details": {
"cross_az_cost": 23.50,
"nat_processing_cost": 89.00,
"internet_egress_cost": 156.00,
"total_data_transfer_cost": 268.50,
"recommendation": "VPC endpoints for S3/DynamoDB would eliminate $89/mo in NAT processing"
}
}
Reservation Purchase Recommendations (RI-007, SP-005)
Query AWS for purchase recommendations instead of just checking coverage gaps.
RI Purchase Recommendation
aws ce get-reservation-purchase-recommendation \
--service "Amazon Elastic Compute Cloud - Compute" \
--term-in-years ONE_YEAR \
--payment-option PARTIAL_UPFRONT \
--lookback-period-in-days SIXTY_DAYS
Savings Plan Purchase Recommendation
aws ce get-savings-plans-purchase-recommendation \
--savings-plans-type COMPUTE_SP \
--term-in-years ONE_YEAR \
--payment-option PARTIAL_UPFRONT \
--lookback-period-in-days SIXTY_DAYS
These return specific amounts and estimated savings. Include in findings as actionable recommendations.
Live Dependency Checks (MANDATORY SAFETY)
Before recommending deletion, verify NO active dependencies.
Dependency Check Matrix
| Resource | Check Command | If Found | Action |
|---|---|---|---|
| EC2 | aws autoscaling describe-auto-scaling-instances --instance-ids {id} | ASG member | SKIP |
| NAT Gateway | aws ec2 describe-route-tables --filters "Name=route.nat-gateway-id,Values={id}" | Routes exist | SKIP |
| ELB | aws elbv2 describe-target-health --target-group-arn {arn} | Healthy targets | SKIP |
| EBS Snapshot | aws ec2 describe-images --filters "Name=block-device-mapping.snapshot-id,Values={id}" | AMI uses it | Add warning |
| EFS | aws efs describe-mount-targets --file-system-id {id} | Mount targets | Add warning |
Output When Blocked
{
"check_id": "EC2-001",
"resource_id": "i-abc123",
"dependency_check": {
"type": "asg_membership",
"result": "member_of: my-asg",
"safe_to_recommend": false
},
"status": "skipped",
"skip_reason": "Resource is member of Auto Scaling Group 'my-asg'"
}
DO NOT
- Recommend deleting EC2 without checking ASG membership
- Recommend deleting NAT without checking route tables
- Recommend deleting ELB without checking target health
- Skip dependency checks to save API calls
ZERO HALLUCINATION PRICING SYSTEM
ABSOLUTE RULE: Never invent, estimate, guess, or approximate a price. Every number must come from a verifiable source.
Pricing Resolution Order (MANDATORY)
For EVERY finding that has a monthly_savings value, you MUST resolve the price using this exact order. Do NOT skip steps.
Step 1: Query AWS Pricing API for the EXACT resource SKU
↓ (if API returns a price → USE IT, set pricing_source: "aws_pricing_api")
↓ (if API fails or returns empty → go to Step 2)
Step 2: Look up in the Verified Pricing Table below
↓ (if found → USE IT, set pricing_source: "verified_table")
↓ (if NOT found → go to Step 3)
Step 3: Set monthly_savings: 0, add pricing_unknown: true
DO NOT GUESS. DO NOT INTERPOLATE. DO NOT "ESTIMATE".
Step 1: AWS Pricing API Queries
ALWAYS try these first. Use --region us-east-1 (Pricing API endpoint).
EC2 Instance Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonEC2 \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
"Type=TERM_MATCH,Field=operatingSystem,Value={OS}" \
"Type=TERM_MATCH,Field=tenancy,Value=Shared" \
"Type=TERM_MATCH,Field=preInstalledSw,Value=NA" \
"Type=TERM_MATCH,Field=capacitystatus,Value=Used" \
--max-results 1
You MUST set the correct values:
{ACTUAL_INSTANCE_TYPE}: The EXACT instance type fromdescribe-instances(e.g., m6i.large, c7g.xlarge){OS}: Check thePlatformfield from describe-instances. Ifwindows→Windows, if absent →Linux{LOCATION_NAME}: Map from the region being scanned (see Region Map below)
RDS Instance Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonRDS \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_DB_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
"Type=TERM_MATCH,Field=databaseEngine,Value={ACTUAL_ENGINE}" \
"Type=TERM_MATCH,Field=deploymentOption,Value={ACTUAL_DEPLOYMENT}" \
--max-results 1
You MUST set the correct values:
{ACTUAL_ENGINE}: Fromdescribe-db-instances→Enginefield. Map:postgres→PostgreSQL,mysql→MySQL,mariadb→MariaDB,aurora-postgresql→Aurora PostgreSQL,aurora-mysql→Aurora MySQL{ACTUAL_DEPLOYMENT}: Fromdescribe-db-instances→MultiAZfield. Iftrue→Multi-AZ, iffalse→Single-AZ
EBS Volume Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonEC2 \
--filters \
"Type=TERM_MATCH,Field=volumeApiName,Value={ACTUAL_VOLUME_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 5
ElastiCache Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonElastiCache \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_NODE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
"Type=TERM_MATCH,Field=cacheEngine,Value={ACTUAL_ENGINE}" \
--max-results 1
OpenSearch Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonES \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
DocumentDB Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonDocDB \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
Neptune Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonNeptune \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
Redshift Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonRedshift \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_NODE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
SageMaker Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonSageMaker \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
MSK (Kafka) Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonMSK \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value={ACTUAL_INSTANCE_TYPE}" \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 1
FSx Pricing
aws pricing get-products --region us-east-1 \
--service-code AmazonFSx \
--filters \
"Type=TERM_MATCH,Field=location,Value={LOCATION_NAME}" \
--max-results 10
Parsing the Pricing API Response
The response contains nested JSON. Extract the On-Demand hourly price:
PriceList[0] → parse as JSON → terms.OnDemand → first value → priceDimensions → first value → pricePerUnit.USD
monthly_cost = hourly_price × 730
If the response is empty (PriceList: []), the API did not find a match. Go to Step 2.
Region Location Map (COMPLETE)
| Region Code | Location Name |
|---|---|
| us-east-1 | US East (N. Virginia) |
| us-east-2 | US East (Ohio) |
| us-west-1 | US West (N. California) |
| us-west-2 | US West (Oregon) |
| eu-west-1 | EU (Ireland) |
| eu-west-2 | EU (London) |
| eu-west-3 | EU (Paris) |
| eu-central-1 | EU (Frankfurt) |
| eu-central-2 | EU (Zurich) |
| eu-north-1 | EU (Stockholm) |
| eu-south-1 | EU (Milan) |
| ap-southeast-1 | Asia Pacific (Singapore) |
| ap-southeast-2 | Asia Pacific (Sydney) |
| ap-northeast-1 | Asia Pacific (Tokyo) |
| ap-northeast-2 | Asia Pacific (Seoul) |
| ap-northeast-3 | Asia Pacific (Osaka) |
| ap-south-1 | Asia Pacific (Mumbai) |
| ap-east-1 | Asia Pacific (Hong Kong) |
| sa-east-1 | South America (Sao Paulo) |
| ca-central-1 | Canada (Central) |
| ca-west-1 | Canada West (Calgary) |
| me-south-1 | Middle East (Bahrain) |
| me-central-1 | Middle East (UAE) |
| af-south-1 | Africa (Cape Town) |
| il-central-1 | Israel (Tel Aviv) |
| ap-south-2 | Asia Pacific (Hyderabad) |
| ap-southeast-3 | Asia Pacific (Jakarta) |
| ap-southeast-4 | Asia Pacific (Melbourne) |
| eu-south-2 | Europe (Spain) |
If the region is NOT in this map, do NOT guess the location name. Set pricing_unknown: true.
Step 2: Verified Pricing Table (Fallback ONLY)
Use this table ONLY if the Pricing API returned no results. These are us-east-1 On-Demand prices.
If the resource is NOT in us-east-1 and the API failed, set pricing_unknown: true. Do NOT use this table for other regions.
EC2 Instances (Linux, us-east-1)
| Instance Type | Hourly | Monthly (×730) |
|---|---|---|
| t2.nano | $0.0058 | $4.23 |
| t2.micro | $0.0116 | $8.47 |
| t3.nano | $0.0052 | $3.80 |
| t3.micro | $0.0104 | $7.59 |
| t3.small | $0.0208 | $15.18 |
| t3.medium | $0.0416 | $30.37 |
| t3.large | $0.0832 | $60.74 |
| m5.large | $0.096 | $70.08 |
| m5.xlarge | $0.192 | $140.16 |
| r5.large | $0.126 | $91.98 |
| r5.xlarge | $0.252 | $183.96 |
RDS Instances (PostgreSQL, Single-AZ, us-east-1)
| Instance Type | Hourly | Monthly |
|---|---|---|
| db.t3.micro | $0.017 | $12.41 |
| db.t3.small | $0.034 | $24.82 |
| db.t3.medium | $0.068 | $49.64 |
| db.r5.large | $0.25 | $182.50 |
| db.r5.xlarge | $0.50 | $365.00 |
| db.r5.2xlarge | $1.00 | $730.00 |
Fixed-Price Resources (all regions)
| Resource | Monthly | Source |
|---|---|---|
| EIP (unattached) | $3.60 | $0.005/hr × 730 = $3.65 (note: some regions may vary) |
| EKS Cluster | $73.00 | $0.10/hr × 730 = $73.00 |
Per-Unit Storage (us-east-1)
| Type | Price | Unit |
|---|---|---|
| EBS gp3 | $0.08 | GB-month |
| EBS gp2 | $0.10 | GB-month |
| EBS io2 | $0.125 | GB-month (storage only, IOPS separate) |
| EBS snapshot | $0.05 | GB-month |
| S3 Standard | $0.023 | GB-month (first 50TB) |
| S3 IA | $0.0125 | GB-month |
| EFS | $0.30 | GB-month |
| CW Logs Storage | $0.03 | GB-month |
| CW Logs Ingestion | $0.50 | GB (NOT recurring, NOT saveable by retention) |
Per-Unit Compute (us-east-1)
| Type | Price | Unit |
|---|---|---|
| Fargate vCPU | $0.04048 | per vCPU-hour |
| Fargate GB | $0.004445 | per GB-hour |
| Lambda x86 | $0.0000166667 | per GB-second |
| Lambda ARM | $0.0000133334 | per GB-second |
| Lambda requests | $0.20 | per 1M requests |
| Kinesis shard | $0.015 | per shard-hour |
Step 3: When Price Is Unknown
If the instance type, DB engine, node type, or resource SKU is NOT found in Step 1 (API) AND NOT found in Step 2 (table), you MUST:
{
"check_id": "EC2-001",
"resource_id": "i-abc123",
"monthly_savings": 0,
"confidence": 50,
"details": {
"pricing_unknown": true,
"pricing_resolution_attempted": [
"aws_pricing_api: no results for c6gd.2xlarge in EU (Frankfurt)",
"verified_table: c6gd.2xlarge not listed"
],
"instance_type": "c6gd.2xlarge",
"recommendation": "Resource appears idle but savings cannot be calculated - price lookup required"
}
}
NEVER do any of these:
- "A c6gd.2xlarge is similar to a c5.2xlarge, so I'll use that price" → NO
- "Based on the family pattern, this probably costs about $X" → NO
- "I'll estimate conservatively at $Y" → NO
- "A 2xlarge is roughly 2× an xlarge" → NO
ANTI-HALLUCINATION RULES (MANDATORY)
These rules are NON-NEGOTIABLE. Violating any of them produces invalid findings.
Rule 1: Every Number Must Have a Source
Every monthly_savings value MUST trace back to one of:
- AWS Pricing API response (best)
- Verified Pricing Table above (acceptable for us-east-1)
- AWS Cost Explorer response (for reservation/SP recommendations)
- $0 with
pricing_unknown: true(when price cannot be determined)
There is NO fifth option. You cannot derive, interpolate, or estimate a price from any other source.
Rule 2: Never Treat Missing Data as Zero
When a CloudWatch metric returns empty Datapoints: []:
- DO NOT assume the value is 0
- DO NOT treat it as "no activity"
- DO skip that signal in idle score calculation
- DO reduce the denominator (available signals) accordingly
- DO note in details:
"signal_name": {"value": null, "status": "no_data", "action": "skipped"}
If fewer than 2 signals have data, do NOT generate an idle finding. Set:
{
"status": "insufficient_data",
"skip_reason": "Only 1 of 5 signals returned data - cannot determine idle status"
}
Rule 3: Never Pick a Creative Downsize Target
For over-provisioned findings, you may ONLY recommend:
- The EXACT target from AWS Compute Optimizer (if enrolled and recommendation exists)
- One size down in the SAME instance family (e.g., m5.xlarge → m5.large, NEVER m5.xlarge → t3.large)
- Nothing — if neither option is available, flag as over-provisioned but set
monthly_savings: 0with"savings_note": "Manual rightsizing analysis required - no automated target available"
NEVER do:
- Jump across instance families (m5 → t3, r5 → m5)
- Skip multiple sizes (xlarge → small)
- Suggest a completely different instance type based on "workload analysis"
Rule 4: Use Fixed Conservative Rates for Migration Savings
These migration types have variable real-world savings. Use ONLY these fixed rates:
| Migration Type | Fixed Rate | Do NOT Use |
|---|---|---|
| Graviton (x86 → ARM) | 20% | "up to 40%" |
| Spot instances | 50% | "up to 90%" |
| ARM Lambda | 20% | "34% better price-performance" |
| Previous gen upgrade | 10% | "up to 40% better price-performance" |
| GP2 → GP3 | 20% | varies by size |
| S3 Standard → IA | 40% storage only | "save on everything" |
| RI 1yr Partial Upfront | 35% | "up to 72%" |
| SP 1yr No Upfront | 25% | "up to 66%" |
ALWAYS note: "savings_type": "estimated_migration", "fixed_rate_used": "20%_graviton"
Rule 5: Never Estimate Data Transfer Composition
For NAT Gateway, Cross-AZ, and data transfer findings:
- Report the total cost from Cost Explorer (this is a FACT, not an estimate)
- DO NOT estimate what percentage of traffic is S3, DynamoDB, or other services
- DO NOT claim "VPC endpoints would save $X" unless you can verify the specific traffic type
- DO flag as
"needs_manual_review": truewith recommendation to analyze traffic logs
{
"check_id": "NET-016",
"monthly_savings": 0,
"details": {
"nat_total_cost": 89.00,
"nat_total_cost_source": "aws_cost_explorer",
"needs_manual_review": true,
"recommendation": "NAT Gateway costs $89/mo. Analyze VPC Flow Logs to identify which traffic could use VPC Gateway Endpoints (free for S3/DynamoDB). Cannot estimate savings without traffic breakdown."
}
}
Rule 6: Every Finding Must Include Machine-Verifiable Calculation
{
"monthly_savings": 70.08,
"details": {
"calculation": "0.096 × 730 = 70.08",
"calculation_inputs": {
"hourly_rate": 0.096,
"hours_per_month": 730
},
"pricing_source": "aws_pricing_api",
"pricing_api_query": "instanceType=m5.large, location=US East (N. Virginia), OS=Linux"
}
}
Every calculation MUST have:
calculation: Human-readable formula with actual numberscalculation_inputs: Machine-parseable input valuespricing_source: One ofaws_pricing_api,verified_table,aws_cost_explorer,pricing_unknownpricing_api_query: The filters used (if source isaws_pricing_api)
Rule 7: Never Generate a Finding Without Raw API Evidence
Every finding MUST reference data from an actual AWS API response:
resource_idMUST come from an AWS API response (describe-instances, describe-volumes, etc.)instance_typeMUST come from the API response, NOT from your memory or assumptions- Metric values MUST come from CloudWatch API responses
- Cost data MUST come from Cost Explorer API responses
If you cannot trace a value back to a specific API call you made in this session, do NOT include it.
Rule 8: Sanity Check - Savings Cannot Exceed Service Spend
BEFORE reporting ANY finding, verify:
finding.monthly_savings <= service_monthly_spend
Query the actual service spend:
aws ce get-cost-and-usage \
--time-period Start={first_of_last_month},End={first_of_this_month} \
--granularity MONTHLY \
--metrics UnblendedCost \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["{SERVICE_NAME}"]}}'
Service name mappings:
| Domain | Cost Explorer Service Name |
|---|---|
| EC2 instances | Amazon Elastic Compute Cloud - Compute |
| EBS volumes | Amazon Elastic Compute Cloud - Compute |
| RDS | Amazon Relational Database Service |
| ElastiCache | Amazon ElastiCache |
| Lambda | AWS Lambda |
| S3 | Amazon Simple Storage Service |
| CloudWatch | AmazonCloudWatch |
| NAT Gateway | Amazon Elastic Compute Cloud - Compute |
| OpenSearch | Amazon OpenSearch Service |
| Redshift | Amazon Redshift |
| DynamoDB | Amazon DynamoDB |
| EFS | Amazon Elastic File System |
| SageMaker | Amazon SageMaker |
| EKS | Amazon Elastic Container Service for Kubernetes |
| Kinesis | Amazon Kinesis |
| MSK | Amazon Managed Streaming for Apache Kafka |
| Glue | AWS Glue |
| DocumentDB | Amazon DocumentDB (with MongoDB compatibility) |
| Neptune | Amazon Neptune |
| FSx | Amazon FSx |
If the finding's monthly_savings exceeds the service's actual monthly spend:
- Count resources in that service from your resource inventory
- If 1-2 resources in service → savings CAN be up to 100% of service spend (a single idle RDS instance IS the entire RDS spend)
- If 3+ resources in service → cap at 90% and add
"requires_validation": true - If savings > 110% of service spend → the finding is WRONG, recalculate using correct formula
NEVER blindly cap at a fixed percentage — verify resource count first.
Rule 9: Distinguish Storage vs Ingestion (CloudWatch Logs)
CloudWatch Logs has TWO completely separate cost components:
- Ingestion: $0.50/GB — charged ONCE when logs arrive. NOT affected by retention policy.
- Storage: $0.03/GB-month — charged MONTHLY for stored data. Reduced by shorter retention.
Setting retention to 30 days ONLY reduces storage costs. The formula is:
savings = stored_gb × $0.03
NEVER use $0.50 in a retention-related savings calculation.
Rule 10: EBS Pricing Must Include ALL Components
EBS pricing has up to 3 components. You MUST account for all of them:
| Component | gp2 | gp3 | io2 |
|---|---|---|---|
| Storage | $0.10/GB | $0.08/GB | $0.125/GB |
| IOPS | Included (burst) | Free up to 3000, then $0.005/IOPS | $0.065/IOPS |
| Throughput | Included | Free up to 125 MB/s, then $0.04/MBps | Included |
For unattached EBS findings, calculate ALL components:
gp3_cost = (size_gb × 0.08) + (max(0, iops - 3000) × 0.005) + (max(0, throughput - 125) × 0.04)
Get IOPS and throughput from the describe-volumes response (Iops and Throughput fields).
Rule 11: RDS Pricing Must Use Correct Deployment Option
- Check the
MultiAZfield fromdescribe-db-instances - If
MultiAZ: true, query Pricing API withdeploymentOption=Multi-AZ(cost is ~2× Single-AZ) - NEVER use Single-AZ pricing for a Multi-AZ instance
Rule 12: OS-Specific EC2 Pricing
- Check the
Platformfield fromdescribe-instances - If
Platform: windows→ query withoperatingSystem=Windows(typically 2× Linux price) - If
Platformis absent → query withoperatingSystem=Linux - NEVER use Linux pricing for Windows instances
Rule 13: Minimum Savings Floor
Do NOT report findings with monthly_savings < $1.00 (unless monthly_savings is exactly $0 with pricing_unknown: true). These create noise without actionable value.
Rule 14: Flag Findings > $100 for Review
Any finding with monthly_savings > $100 MUST include:
{
"requires_validation": true,
"validation_reason": "Savings exceed $100 threshold"
}
Rule 15: Check RI/SP Coverage Before Calculating Savings
Resources covered by Reserved Instances or Savings Plans cost LESS than On-Demand. Terminating an RI-covered instance doesn't save the On-Demand price — the RI payment continues.
Step 1: Check InstanceLifecycle from describe-instances:
- If absent → On-Demand
- If
spot→ Spot instance (see Rule 16)
Step 2: For On-Demand instances, check if RI/SP coverage exists for this instance type:
aws ce get-reservation-coverage \
--time-period Start={first_of_last_month},End={first_of_this_month} \
--granularity MONTHLY \
--filter '{"Dimensions":{"Key":"INSTANCE_TYPE","Values":["{instance_type}"]}}'
Step 3: If CoverageHoursPercentage > 80%:
- Add
"covered_by_ri_or_sp": trueto finding details - Add
"savings_assumption": "on_demand_pricing_used_but_ri_sp_covers_this_type" - Reduce confidence by -20%
Rule 16: Check for Spot Instances
From describe-instances, check InstanceLifecycle:
- If
"spot"→ actual cost is 50-90% lower than On-Demand - Use
On-Demand_price × 0.3as conservative Spot cost estimate - Add
"instance_lifecycle": "spot"to details
Rule 17: Handle Burstable Instances (t2/t3/t4g)
For instance types starting with t2., t3., t3a., t4g.:
Check CPU Credit Balance trend:
aws cloudwatch get-metric-statistics --namespace AWS/EC2 \
--metric-name CPUCreditBalance --dimensions Name=InstanceId,Value={id} \
--start-time {14d_ago} --end-time {now} --period 86400 --statistics Average
- Credits increasing over 14 days → confirms idle (credits accumulating)
- Credits decreasing or stable → may burst periodically, reduce confidence by -15%
- Credit data unavailable → reduce confidence by -10% for burstable instances
Rule 18: Downsize Floor Check
Before recommending a downsize, verify the target size EXISTS:
- Size order:
nano → micro → small → medium → large → xlarge → 2xlarge → ... - If already at smallest size in family (e.g., t3.nano, c7g.large), set
monthly_savings: 0with"downsize_not_possible": "already_at_minimum_size" - If unsure whether target exists, query the Pricing API — empty result = size doesn't exist
Rule 19: Detect IaC-Managed Resources
Check resource tags for infrastructure-as-code management:
| Tag Key | Meaning |
|---|---|
aws:cloudformation:stack-name | CloudFormation managed |
Tags containing terraform | Terraform managed |
Tags containing pulumi | Pulumi managed |
If IaC tags found:
- Add
"managed_by_iac": trueto details - Change recommendation to "Review with IaC team — direct deletion will trigger recreation"
- Reduce confidence by -10%
Rule 20: Pricing API Error Handling
If aws pricing get-products returns an ERROR (not empty results):
- Throttling (
ThrottlingException): Wait 2 seconds, retry ONCE. If still fails → go to Step 2 (table) - Access denied: Skip to Step 2 immediately
- Any other error: Skip to Step 2 immediately
- Add
"pricing_api_status": "error_type"to details
Rule 21: Cost Explorer Availability
Before generating ANY findings, verify Cost Explorer returns data:
aws ce get-cost-and-usage \
--time-period Start={yesterday},End={today} \
--granularity DAILY \
--metrics UnblendedCost
If empty or DataUnavailableException:
- Set
"cost_explorer_available": falsein metadata - Skip Rule 8 (sanity check against service spend)
- Reduce ALL confidence scores by -15%
- Add to every finding:
"billing_validation_skipped": "cost_explorer_unavailable"
Common Pricing Mistakes
| # | Mistake | Correct Approach |
|---|---|---|
| 1 | CW Logs: stored_gb × $0.50 | stored_gb × $0.03 (storage, not ingestion) |
| 2 | Savings > service spend | Verify resource count first, cap only if 3+ resources |
| 3 | Missing instance type → guess | monthly_savings=0, pricing_unknown=true |
| 4 | Missing metric → assume idle | Skip that signal, note as no_data |
| 5 | Creative downsize (m5→t3) | Same family, one size down only |
| 6 | NAT traffic composition guess | needs_manual_review=true |
| 7 | Windows priced as Linux | Query API with operatingSystem=Windows |
| 8 | Single-AZ price for Multi-AZ | Query API with deploymentOption=Multi-AZ |
| 9 | EBS storage only | Include IOPS + throughput costs |
| 10 | "Up to X%" percentages | Use fixed conservative rates |
| 11 | Interpolating sizes | Query Pricing API for exact type |
| 12 | us-east-1 table for other regions | Use Pricing API for non-us-east-1 |
| 13 | On-Demand price for Spot/RI | Check InstanceLifecycle and RI coverage |
| 14 | Delete IaC-managed resource | Flag for IaC team review |
Rules
- Only return confidence >= 50
- Include Name tags in
resource_name - Return
[]if no issues - Price resolution: API first → verified table → $0 with pricing_unknown
- Every finding needs calculation + pricing_source in details
- Sanity-check savings vs service billing (context-aware, not fixed cap)
- Flag findings > $100 for validation
- Missing CloudWatch data = skip signal, not zero
- Downsize: same family, one size down, verify target exists
- Data transfer composition: flag for manual review, not estimate
- Correct OS, deployment option, all EBS components
- No findings with monthly_savings between $0.01 and $0.99
- Check RI/SP coverage and Spot lifecycle before pricing
- Detect burstable instance credit patterns
- Flag IaC-managed resources for team review
- Handle Pricing API errors gracefully (retry then fallback)
- Verify Cost Explorer availability before sanity checks