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_advanced
  • region: AWS region to scan
  • compliance: [HIPAA, SOC2, PCI-DSS] or empty
  • profile: AWS profile name

11 Domains (173 checks total)

DomainChecksResources
compute27EC2, EBS, AMIs, snapshots, EIPs, Compute Optimizer
storage24S3, EFS, CloudWatch Logs, CloudTrail, Secrets Manager
database15RDS, DynamoDB, ElastiCache
networking18NAT, ELB, VPC endpoints, data transfer, Route 53
serverless10Lambda, API Gateway, SQS, Step Functions
reservations12RI coverage, Savings Plans, purchase recommendations
containers16ECS, EKS, Fargate, ECR
advanced_databases18Aurora, DocumentDB, Neptune, Redshift
analytics15SageMaker, EMR, OpenSearch, QuickSight
data_pipelines12Kinesis, MSK, Glue, EventBridge
storage_advanced6FSx, AWS Backup

Workflow

  1. Read checks/all_checks.yaml for domain checks
  2. Run AWS CLI commands via MCP tool
  3. Save resource inventory
  4. Compare against thresholds
  5. Return findings + resources

Compliance Rules

ComplianceRuleskip_reason
HIPAASkip phi=true tags, healthcare nameshipaa-protected
SOC2Don't delete logs (but CAN set retention)soc2-audit-required
PCI-DSSSkip pci=true tags, payment VPCspci-dss-protected

Tag-Based Exclusions

SkipCostOpt Tag

Resources with SkipCostOpt=true are excluded from ALL checks.

Exclusion Tags (Honor Any)

Tag KeyTruthy Values
SkipCostOpttrue, 1, yes
CostOptExcludetrue, 1, yes
DoNotOptimizetrue, 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 instances
  • ebs_volumes: All volumes
  • elastic_ips: All EIPs
  • snapshots: Account snapshots
  • amis: Account AMIs
  • compute_optimizer_status: Enrollment status (Active/Inactive)
  • compute_optimizer_recommendations: CO recommendations (if enrolled)

storage

  • s3_buckets: All buckets
  • efs_filesystems: All EFS
  • cloudwatch_log_groups: All log groups with retention
  • cloudtrail_trails: All trails
  • secrets: Secrets Manager secrets (with last accessed date)

database

  • rds_instances: All RDS
  • rds_snapshots: Manual snapshots
  • dynamodb_tables: All tables
  • elasticache_clusters: All clusters

networking

  • nat_gateways: All NAT gateways
  • load_balancers: ALBs/NLBs
  • vpc_endpoints: All endpoints
  • data_transfer_costs: USAGE_TYPE breakdown from Cost Explorer
  • route53_zones: Hosted zones with record counts

serverless

  • lambda_functions: All functions
  • api_gateways: REST/HTTP APIs
  • sqs_queues: All queues
  • step_functions: State machines

reservations

  • reserved_instances: EC2 RIs
  • reserved_db_instances: RDS RIs
  • savings_plans: Active plans
  • ri_purchase_recommendations: AWS-generated RI recommendations
  • sp_purchase_recommendations: AWS-generated SP recommendations

containers

  • ecs_clusters: All ECS clusters
  • ecs_services: Services per cluster
  • ecs_task_definitions: Task definitions
  • eks_clusters: All EKS clusters
  • eks_nodegroups: Node groups per cluster
  • fargate_tasks: Fargate tasks
  • ecr_repositories: ECR repos with lifecycle policy status

advanced_databases

  • aurora_clusters: Aurora DB clusters
  • aurora_instances: Aurora instances
  • documentdb_clusters: DocumentDB clusters
  • neptune_clusters: Neptune clusters
  • redshift_clusters: Redshift clusters

analytics

  • sagemaker_notebooks: Notebook instances
  • sagemaker_endpoints: Inference endpoints
  • emr_clusters: Active EMR clusters
  • opensearch_domains: OpenSearch domains
  • quicksight_datasets: QuickSight datasets

data_pipelines

  • kinesis_streams: Data streams
  • firehose_streams: Firehose delivery streams
  • msk_clusters: MSK Kafka clusters
  • glue_jobs: Glue ETL jobs
  • eventbridge_rules: EventBridge rules

storage_advanced

  • fsx_filesystems: FSx file systems
  • backup_plans: AWS Backup plans
  • backup_vaults: Backup vaults

Cost-Tiered Confidence

Higher-cost findings need MORE evidence before flagging.

Confidence Tiers

TierMonthly CostMin AgeMin ConfidenceSignals Required
LOW< $201 day65%1 signal OK
MEDIUM$20 - $1003 days75%2+ signals
HIGH> $1007 days85%2+ signals

Apply Before Reporting

  1. Calculate monthly_savings → determine tier
  2. Check resource age >= tier.min_age_days
  3. Check confidence >= tier.min_confidence
  4. For MEDIUM/HIGH: verify 2+ signals agree
  5. 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

SignalMetricThresholdWeight
CPUCPUUtilization avg< 5%0.40
Network InNetworkIn< 0.1 GB/day0.15
Network OutNetworkOut< 0.1 GB/day0.15
ConnectionsNetworkPacketsIn00.20
Disk I/ODiskReadOps+WriteOps< 10/sec0.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 IDLE
  • recommendationOptions: 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:

SignalMetricThresholdWeight
Memorymem_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 TypeWhat It IsCost
DataTransfer-Regional-BytesCross-AZ traffic$0.01/GB each direction
NatGateway-BytesNAT data processing$0.045/GB
DataTransfer-Out-BytesInternet egress$0.09/GB first 10TB
DataTransfer-In-BytesInternet ingressFree

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

ResourceCheck CommandIf FoundAction
EC2aws autoscaling describe-auto-scaling-instances --instance-ids {id}ASG memberSKIP
NAT Gatewayaws ec2 describe-route-tables --filters "Name=route.nat-gateway-id,Values={id}"Routes existSKIP
ELBaws elbv2 describe-target-health --target-group-arn {arn}Healthy targetsSKIP
EBS Snapshotaws ec2 describe-images --filters "Name=block-device-mapping.snapshot-id,Values={id}"AMI uses itAdd warning
EFSaws efs describe-mount-targets --file-system-id {id}Mount targetsAdd 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 from describe-instances (e.g., m6i.large, c7g.xlarge)
  • {OS}: Check the Platform field from describe-instances. If windowsWindows, 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}: From describe-db-instancesEngine field. Map: postgresPostgreSQL, mysqlMySQL, mariadbMariaDB, aurora-postgresqlAurora PostgreSQL, aurora-mysqlAurora MySQL
  • {ACTUAL_DEPLOYMENT}: From describe-db-instancesMultiAZ field. If trueMulti-AZ, if falseSingle-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 CodeLocation Name
us-east-1US East (N. Virginia)
us-east-2US East (Ohio)
us-west-1US West (N. California)
us-west-2US West (Oregon)
eu-west-1EU (Ireland)
eu-west-2EU (London)
eu-west-3EU (Paris)
eu-central-1EU (Frankfurt)
eu-central-2EU (Zurich)
eu-north-1EU (Stockholm)
eu-south-1EU (Milan)
ap-southeast-1Asia Pacific (Singapore)
ap-southeast-2Asia Pacific (Sydney)
ap-northeast-1Asia Pacific (Tokyo)
ap-northeast-2Asia Pacific (Seoul)
ap-northeast-3Asia Pacific (Osaka)
ap-south-1Asia Pacific (Mumbai)
ap-east-1Asia Pacific (Hong Kong)
sa-east-1South America (Sao Paulo)
ca-central-1Canada (Central)
ca-west-1Canada West (Calgary)
me-south-1Middle East (Bahrain)
me-central-1Middle East (UAE)
af-south-1Africa (Cape Town)
il-central-1Israel (Tel Aviv)
ap-south-2Asia Pacific (Hyderabad)
ap-southeast-3Asia Pacific (Jakarta)
ap-southeast-4Asia Pacific (Melbourne)
eu-south-2Europe (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 TypeHourlyMonthly (×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 TypeHourlyMonthly
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)

ResourceMonthlySource
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)

TypePriceUnit
EBS gp3$0.08GB-month
EBS gp2$0.10GB-month
EBS io2$0.125GB-month (storage only, IOPS separate)
EBS snapshot$0.05GB-month
S3 Standard$0.023GB-month (first 50TB)
S3 IA$0.0125GB-month
EFS$0.30GB-month
CW Logs Storage$0.03GB-month
CW Logs Ingestion$0.50GB (NOT recurring, NOT saveable by retention)

Per-Unit Compute (us-east-1)

TypePriceUnit
Fargate vCPU$0.04048per vCPU-hour
Fargate GB$0.004445per GB-hour
Lambda x86$0.0000166667per GB-second
Lambda ARM$0.0000133334per GB-second
Lambda requests$0.20per 1M requests
Kinesis shard$0.015per 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:

  1. The EXACT target from AWS Compute Optimizer (if enrolled and recommendation exists)
  2. One size down in the SAME instance family (e.g., m5.xlarge → m5.large, NEVER m5.xlarge → t3.large)
  3. Nothing — if neither option is available, flag as over-provisioned but set monthly_savings: 0 with "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 TypeFixed RateDo NOT Use
Graviton (x86 → ARM)20%"up to 40%"
Spot instances50%"up to 90%"
ARM Lambda20%"34% better price-performance"
Previous gen upgrade10%"up to 40% better price-performance"
GP2 → GP320%varies by size
S3 Standard → IA40% storage only"save on everything"
RI 1yr Partial Upfront35%"up to 72%"
SP 1yr No Upfront25%"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": true with 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 numbers
  • calculation_inputs: Machine-parseable input values
  • pricing_source: One of aws_pricing_api, verified_table, aws_cost_explorer, pricing_unknown
  • pricing_api_query: The filters used (if source is aws_pricing_api)

Rule 7: Never Generate a Finding Without Raw API Evidence

Every finding MUST reference data from an actual AWS API response:

  • resource_id MUST come from an AWS API response (describe-instances, describe-volumes, etc.)
  • instance_type MUST 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:

DomainCost Explorer Service Name
EC2 instancesAmazon Elastic Compute Cloud - Compute
EBS volumesAmazon Elastic Compute Cloud - Compute
RDSAmazon Relational Database Service
ElastiCacheAmazon ElastiCache
LambdaAWS Lambda
S3Amazon Simple Storage Service
CloudWatchAmazonCloudWatch
NAT GatewayAmazon Elastic Compute Cloud - Compute
OpenSearchAmazon OpenSearch Service
RedshiftAmazon Redshift
DynamoDBAmazon DynamoDB
EFSAmazon Elastic File System
SageMakerAmazon SageMaker
EKSAmazon Elastic Container Service for Kubernetes
KinesisAmazon Kinesis
MSKAmazon Managed Streaming for Apache Kafka
GlueAWS Glue
DocumentDBAmazon DocumentDB (with MongoDB compatibility)
NeptuneAmazon Neptune
FSxAmazon FSx

If the finding's monthly_savings exceeds the service's actual monthly spend:

  1. Count resources in that service from your resource inventory
  2. 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)
  3. If 3+ resources in service → cap at 90% and add "requires_validation": true
  4. 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:

Componentgp2gp3io2
Storage$0.10/GB$0.08/GB$0.125/GB
IOPSIncluded (burst)Free up to 3000, then $0.005/IOPS$0.065/IOPS
ThroughputIncludedFree up to 125 MB/s, then $0.04/MBpsIncluded

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 MultiAZ field from describe-db-instances
  • If MultiAZ: true, query Pricing API with deploymentOption=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 Platform field from describe-instances
  • If Platform: windows → query with operatingSystem=Windows (typically 2× Linux price)
  • If Platform is absent → query with operatingSystem=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": true to 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.3 as 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: 0 with "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 KeyMeaning
aws:cloudformation:stack-nameCloudFormation managed
Tags containing terraformTerraform managed
Tags containing pulumiPulumi managed

If IaC tags found:

  • Add "managed_by_iac": true to 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": false in 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

#MistakeCorrect Approach
1CW Logs: stored_gb × $0.50stored_gb × $0.03 (storage, not ingestion)
2Savings > service spendVerify resource count first, cap only if 3+ resources
3Missing instance type → guessmonthly_savings=0, pricing_unknown=true
4Missing metric → assume idleSkip that signal, note as no_data
5Creative downsize (m5→t3)Same family, one size down only
6NAT traffic composition guessneeds_manual_review=true
7Windows priced as LinuxQuery API with operatingSystem=Windows
8Single-AZ price for Multi-AZQuery API with deploymentOption=Multi-AZ
9EBS storage onlyInclude IOPS + throughput costs
10"Up to X%" percentagesUse fixed conservative rates
11Interpolating sizesQuery Pricing API for exact type
12us-east-1 table for other regionsUse Pricing API for non-us-east-1
13On-Demand price for Spot/RICheck InstanceLifecycle and RI coverage
14Delete IaC-managed resourceFlag for IaC team review

Rules

  1. Only return confidence >= 50
  2. Include Name tags in resource_name
  3. Return [] if no issues
  4. Price resolution: API first → verified table → $0 with pricing_unknown
  5. Every finding needs calculation + pricing_source in details
  6. Sanity-check savings vs service billing (context-aware, not fixed cap)
  7. Flag findings > $100 for validation
  8. Missing CloudWatch data = skip signal, not zero
  9. Downsize: same family, one size down, verify target exists
  10. Data transfer composition: flag for manual review, not estimate
  11. Correct OS, deployment option, all EBS components
  12. No findings with monthly_savings between $0.01 and $0.99
  13. Check RI/SP coverage and Spot lifecycle before pricing
  14. Detect burstable instance credit patterns
  15. Flag IaC-managed resources for team review
  16. Handle Pricing API errors gracefully (retry then fallback)
  17. Verify Cost Explorer availability before sanity checks