With the tags activated and the cost and usage report landing every day in mercadofresco-informes-analitica, MercadoFresco can already share out its bill. What is missing is the other half: actually looking at it. Not the total, not a summary table, but the full breakdown by service, going into the three or four lines almost nobody understands, and coming out of there with a list of concrete actions and their calculated savings.

This lesson makes that journey. You will see what Cost Explorer is and what data it works with, the billing concepts you have to understand before looking at a chart — because otherwise the numbers do not add up and you lose confidence in the tool — MercadoFresco's real bill service by service up to 2,237.60 USD, the three surprises that turn up almost every time, anomaly detection, the automatic recommendations and how they relate to Compute Optimizer and Trusted Advisor, the query from boto3 that publishes spend alongside the business figures, and the ten optimisations MercadoFresco carries out, with their savings one by one.

Cost warning. The Cost Explorer interface is free. What does cost money is the API: each paginated call to GetCostAndUsage and friends is billed at 0.01 USD. A dashboard that queries the API every five minutes comes to around 90 USD a month, an expensive and surprisingly common mistake. Anomaly detection is free. Fictitious data, accounts and identifiers; prices are indicative and vary by region and date.

Contents

  1. What Cost Explorer is and what data it works with
  2. Billing concepts: unblended, amortised and net
  3. Charges, credits, refunds and taxes: why the total does not add up
  4. Consolidated billing across the six accounts
  5. MercadoFresco's bill, service by service
  6. Surprise 1: the NAT Gateways
  7. Surprise 2: cross-AZ data transfer
  8. Surprise 3: CloudWatch
  9. Filters, groupings and saved reports
  10. The cost dashboard Marta reviews every month
  11. Cost anomaly detection
  12. The case of the nightly Redshift load
  13. Reduction recommendations and orphan resources
  14. Compute Optimizer and Trusted Advisor
  15. Spend forecasting and how to read it
  16. Querying Cost Explorer from boto3
  17. Publishing daily spend as a business metric
  18. MercadoFresco's ten optimisations
  19. Common mistakes and tips
  20. Exercises
  21. Conclusion

What Cost Explorer is and what data it works with

AWS Cost Explorer is the interactive analysis tool for the bill: charts, filters, groupings and forecasts over spend and usage. You enable it once from the management account and, from that moment on, it starts preparing the data.

What you need to know before trusting a number:

Characteristic Detail Practical consequence
History 13 months back by default, extendable to 38 Lets you compare with the same month a year earlier
Monthly and daily granularity Available across the whole history Enough for 90 % of the analysis
Hourly granularity Optional, paid (around 0.01 USD per 1,000 records queried), with 14 days of retention Essential for the Friday peak and for 11-05
Refresh delay Up to 24 hours, sometimes 48 at month end Today's spend is not complete; comparing partial days misleads
Forecast Up to 12 months ahead Useful with caveats; see below
Resource-level data Optional, 14 days When you need more, you go to the CUR (11-02)
Cost of the interface Free The API does cost: 0.01 USD per request

The first operational rule that saves you grief: never draw conclusions from the current day. With the refresh delay, today's spend always looks lower than it will be, and more than one team has celebrated a saving that was simply incomplete data.

Billing concepts: unblended, amortised and net

Before looking at a chart you have to understand what it is measuring, because Cost Explorer offers several cost metrics and they give different results for the same month. It is the number one cause of distrust in the tool.

Metric What it measures When to use it
Unblended cost The cost exactly as it is billed, when it is billed Reconciling with the month's bill; the default value
Amortised cost Spreads the upfront payments for reservations and Savings Plans across the period they cover Analysing the real cost of operating; essential once you have commitments
Net unblended cost Same as unblended, but with discounts and credits deducted Seeing what you actually pay
Net amortised cost Amortised and with discounts applied The most faithful metric for long-term tracking
Blended cost Average of rates across the organisation's accounts Almost never; it exists for historical reasons

The difference is clearest with an example MercadoFresco will live through in 11-05. Suppose a 1-year Savings Plan with an all upfront payment of 1,200 USD taken out on 1 March:

Month Unblended cost Amortised cost
March 1,200 USD (the whole payment) 100 USD
April 0 USD 100 USD
May 0 USD 100 USD

With unblended cost, the chart shows an enormous spike in March and a drop to zero afterwards, which is true for the cash flow and utterly useless for analysing efficiency. With amortised cost, each month carries the 100 USD that belong to it and the chart reflects the real cost of operating.

The practical recommendation: use unblended to reconcile with the bill and net amortised to analyse. And always say which one you are using when you present a number, because two people looking at the same screen with different metrics will argue for half an hour without realising they are both right.

MercadoFresco, which has no commitments purchased yet, sees exactly the same number in all four metrics. That changes in 11-05, and it is worth knowing in advance.

Charges, credits, refunds and taxes: why the total does not add up

The second source of distrust is that the Cost Explorer total does not match the amount charged to the card. It is not an error: the bill contains line types that Cost Explorer, by default, filters out or shows separately.

Line type What it is Shown by default?
Usage Actual consumption of a service Yes
Tax Taxes (VAT, in the Spanish case) No, excluded by default
Credit Promotional, programme or compensation credits Can be included or excluded
Refund Refunds for errors or adjustments Can be included or excluded
RIFee / SavingsPlanUpfrontFee Fee for a reservation or a Savings Plan Yes, with caveats depending on the metric
Support Support plan fee Yes
Enterprise Discount Negotiated discount Only in the "net" metrics

MercadoFresco's August bill, in full:

Service costs (Usage) ........................... 2,237.60 USD
Credits applied ................................. -   45.00 USD  (activation credits)
Support (Basic plan) ............................       0.00 USD
Subtotal ........................................ 2,192.60 USD
Tax (VAT 21 %) .................................. +  460.45 USD
------------------------------------------------------------
Total charged ................................... 2,653.05 USD

Two rules come out of that which are worth fixing in the team's head:

  1. The working number is the service cost: 2,237.60 USD. That is the one you can reduce with engineering decisions. Taxes are not optimised, they are paid.
  2. Credits deceive. While they last, spend looks lower than it is, and the day they run out a jump appears that nobody caused. MercadoFresco excludes them from the analysis and treats them as what they are: a temporary discount.

Consolidated billing across the six accounts

The organisation o-a1b2c3d4e5 has consolidated billing enabled, which is the default behaviour of AWS Organizations (09-04). That implies three things which bear directly on this analysis:

  • A single bill and a single payment method, issued from the management account 999988887777. Member accounts do not receive their own bill.
  • Usage aggregation for volume tiers. The 40 GB of S3 in production, the 12 in pre-production and the 8 in development count as 60 GB for pricing tiers. At small volumes the saving is symbolic, but with data transfer or S3 at scale it becomes relevant.
  • Commitments are shared. A Savings Plan bought in any account covers usage in all the others, unless sharing is turned off. It is a central point of 11-05.

In Cost Explorer, the management account sees everything; a member account only sees itself, and only if access is enabled. Marta always works from the management account with the billing read-only role, and Luis and Sara see their own account.

MercadoFresco's bill, service by service

This is the full breakdown for the month of August, grouped by service, with unblended cost and the six accounts consolidated:

# Service USD/month % What it is exactly
1 Amazon Aurora (RDS) 342.60 15.3 % aurora-mercadofresco-pedidos: writer + 2 readers, storage, I/O, backups; plus pre-production and development
2 Amazon ECS – Fargate 262.40 11.7 % svc-mercadofresco-tienda-fg on arm64 and -trabajadores on Spot, across 3 environments
3 NAT Gateway 243.80 10.9 % 6 NATs (2 per environment): hourly charge + data processed
4 Amazon CloudWatch 214.90 9.6 % Logs, custom metrics, Container Insights, dashboards, alarms, canary
5 Amazon ElastiCache 135.40 6.1 % mercadofresco-catalogo: primary + replica, plus a small node in pre-production
6 Amazon S3 128.90 5.8 % 5 buckets: photos, reports, web logs, backups, artefacts
7 VPC: endpoints and cross-AZ transfer 118.60 5.3 % 4 interface endpoints × 2 AZs + cross-zone traffic
8 Amazon Redshift Serverless 104.50 4.7 % wg-mercadofresco-analitica: RPU-hours and managed storage
9 Elastic Load Balancing 104.20 4.7 % alb-mercadofresco-tienda across 3 environments: fixed charge + capacity units
10 AWS Config 74.60 3.3 % grabador-mercadofresco in 6 accounts + 29 rules
11 Amazon CloudFront 62.80 2.8 % Distribution E2QWERTY123ABC: transfer above the free tier
12 Amazon DynamoDB 58.40 2.6 % mercadofresco-carritos and -idempotencia: on-demand + PITR
13 AWS Backup and snapshots 46.80 2.1 % Aurora, EBS and DynamoDB backups, with a cross-region copy
14 AWS Lambda 41.20 1.8 % The course's 6 functions: GB-seconds and invocations
15 Amazon GuardDuty 38.60 1.7 % Event analysis across the 6 accounts
16 AWS KMS 34.60 1.5 % alias/mercadofresco-datos and encryption requests
17 Amazon EC2 and EBS (residual) 33.90 1.5 % One forgotten bastion, 3 stray volumes, 2 elastic IPs
18 AWS WAF 31.20 1.4 % waf-mercadofresco-cdn and -alb: Web ACL, rules and requests
19 Messaging (SQS, SNS, EventBridge, Step Functions) 27.50 1.2 % The queues, the topic, the bus and the state machine
20 Developer tools (CodeBuild, Pipeline, Deploy, ECR) 26.40 1.2 % Build minutes, active pipelines and 42 GB of images
21 Data transfer to the internet (outside CloudFront) 24.80 1.1 % Direct egress from the ALB and from the tasks
22 Amazon Inspector 21.40 1.0 % Continuous scanning of ECR images
23 AWS CloudTrail 18.70 0.8 % trail-mercadofresco, scoped data events and Insights
24 Amazon Route 53 12.40 0.6 % Hosted zone, queries and health checks
25 AWS X-Ray 10.60 0.5 % Traces with optimised sampling
26 Other (Athena, Chatbot, Systems Manager, Cost Explorer API) 9.80 0.4 % Lower-value services
27 Secrets Manager and Parameter Store 8.60 0.4 % 2 secrets with rotation and advanced parameters
Total 2,237.60 100 %

The first thing to do with a table like this is read it the opposite way round from how everyone looks at it. The typical reaction is to fix on Aurora, which is the biggest line, and start arguing about whether two readers are needed. But Aurora is there because of a conscious decision, documented and signed off by management (ADR-014 in 11-01). What is interesting are the lines nobody decided: the NAT, cross-AZ transfer, CloudWatch and the residual EC2. Together they come to 611.20 USD a month, 27.3 % of the bill, and not one of them has ever been the subject of an explicit decision.

Surprise 1: the NAT Gateways

243.80 USD a month, the third line of the bill, for a component nobody remembers choosing. It is the most repeated finding in every cost review in the industry.

The NAT Gateway is billed on two counts:

Item Approximate price in eu-west-1 Comment
Hourly charge ~0.045 USD/h ≈ 33 USD a month per NAT You pay it even if not a single byte goes through
Data processed ~0.045 USD/GB You pay for data in both directions

MercadoFresco's breakdown:

Environment NATs Fixed charge Data processed Total
Production 2 (one per AZ) 66.00 USD 34.00 USD (756 GB) 100.00 USD
Pre-production 2 66.00 USD 8.00 USD (178 GB) 74.00 USD
Development 2 66.00 USD 3.80 USD (85 GB) 69.80 USD
Total 6 198.00 USD 45.80 USD 243.80 USD

Two observations that change the conversation:

  1. 81 % of the cost is the fixed hourly charge, not the traffic. Optimising traffic does not fix the problem; reducing the number of NATs does.
  2. Pre-production and development pay 143.80 USD a month to reach the internet, in environments where there is not a single customer and where an hour without internet access bothers nobody. Two NATs in development is high availability for an environment nobody considers critical.

And there is a third observation that connects with 10-02: a good part of production's 756 GB is image pulls from ECR, S3 API calls and log delivery to CloudWatch, that is, traffic towards AWS services that could bypass the NAT if there were endpoints. MercadoFresco already put four endpoints on the critical path; the analysis still pending is whether more are worth it, and the answer — as seen in 10-02 — is that seven endpoints across two AZs work out dearer than the NAT.

Surprise 2: cross-AZ data transfer

Inside the 118.60 USD of the VPC line there are two very different things:

Item Cost Nature
4 interface endpoints × 2 AZs 64.20 USD Fixed charge per hour and per AZ
Cross-AZ data transfer 54.40 USD ~0.01 USD/GB in each direction

Cross-zone transfer is the most invisible cost in AWS, because there is no resource to blame: it does not appear under any tag, there is no dashboard that shows it and you cannot switch it off. It appears all by itself as soon as an architecture is Multi-AZ, which is exactly what is recommended for reliability.

Where those 54.40 USD come from at MercadoFresco:

  • The ALB spreads traffic across two AZs. With cross-zone load balancing enabled — which on an ALB is always on and free for ALB, but not for NLB — a request arriving in AZ a can end up on a task in AZ b.
  • Aurora replicates from the writer to both readers, and one of them is in the other zone. Every byte written crosses the zone.
  • ElastiCache replicates from the primary to the replica, which is in the other AZ.
  • Fargate tasks read from the Aurora writer, which sits in one specific zone; half the tasks cross.

And here comes the important part: nearly all of that cost is the price of high availability, and it should not be removed. This is the textbook case of the trade-off between pillars from 11-01: you could save by concentrating everything in one zone, and you would lose exactly the property Multi-AZ was built for. What does make sense is reducing unnecessary crossing: routing reads to the reader in the same zone where possible, and not shifting large volumes of data between zones out of habit.

The right action on this line is not to cut it, but to know it exists, understand it and not discover it the day the bill goes up 40 % because of a topology change.

Surprise 3: CloudWatch

214.90 USD, the fourth line of the bill. It is the most uncomfortable surprise, because observability is a virtue and here it shows up as a considerable expense. The breakdown:

CloudWatch item Cost Origin
Log ingestion 118.40 USD ~61 GB/month at ~0.50 USD/GB (Logs Standard)
Log storage 34.60 USD ~1.1 TB accumulated: retention was never configured
Custom metrics 27.00 USD 90 metrics × 0.30 USD
Container Insights 18.60 USD Per-task and per-container metrics
Alarms 8.40 USD 84 alarms × 0.10 USD
Dashboards 6.00 USD 2 dashboards above the free 3, plus old versions
Synthetics canary 1.90 USD A check every 5 minutes

The two dominant lines are log ingestion and log storage, and both have the same root: nobody ever decided how much to keep or what to log. Specifically:

  • The applications log at DEBUG level in pre-production and in production, because it was turned on to debug an incident in April and never turned back down.
  • The ALB access logs and the WAF logs go to CloudWatch Logs as well as to S3, doubling the cost.
  • No log group has retentionInDays configured, which means indefinite retention. A group created a year ago is still keeping everything.

This is the item with the best effort-to-saving ratio in the whole bill, and it appears in the list of optimisations further down.

Filters, groupings and saved reports

The value of Cost Explorer lies in crossing dimensions. The available groupings and what each one is for:

Group by Answers Typical use at MercadoFresco
Service What are we spending on? The table above; the starting point
Linked account Which environment spends? Production 66 %, pre-production 18 %, development 11 %
Region Where do we spend? Spotting forgotten resources in other regions
Usage type What exactly within the service? Separating NatGateway-Hours from NatGateway-Bytes
Charge type Usage, tax, credit or fee? Reconciling with the bill
Tag Which component, or who? Componente, Propietario, Entorno (11-02)
Cost category Which business area? producto, plataforma, analitica
Instance family / platform Which hardware? Seeing how much is still on x86 versus arm64

Usage type is the most underrated dimension and the one that solves the most mysteries. "Amazon Virtual Private Cloud: 362.40 USD" tells you nothing; grouped by usage type you get EUW1-NatGateway-Hours: 198.00, EUW1-NatGateway-Bytes: 45.80, EUW1-VpcEndpoint-Hours: 64.20 and EUW1-DataTransfer-Regional-Bytes: 54.40, and suddenly you know exactly what to do.

Saved reports are queries with their filters and groupings, given a name and shared within the management account. MercadoFresco's five:

Saved report Configuration What for
mf-mensual-por-servicio Monthly, grouped by service, 13 months The overall picture and its trend
mf-por-entorno Monthly, grouped by linked account How much the non-production side takes
mf-por-componente Monthly, grouped by the Componente tag The conversation with each owner
mf-red Monthly, filtered to VPC + ELB + CloudFront, grouped by usage type Watching NAT and transfer
mf-no-productivo-diario Daily, filtered to accounts 2222… and 3333… Spotting what gets left switched on

The last one is the most useful of the five: in a daily view of development, a resource left on over a Saturday is visible at a glance.

The cost dashboard Marta reviews every month

Marta blocks out 45 minutes on the 5th of each month — when the previous month's data is already closed — and always follows the same script. The discipline of always looking at the same things, in the same order, is what turns noise into a trend:

  1. Month total against the previous month and against the same month last year. If the variation exceeds ±10 %, it gets explained before moving on.
  2. Cost per order. It is the first number said out loud, before the total (11-02).
  3. The five lines that have risen most in absolute value, not in percentage. A service going from 0.40 to 1.20 USD has risen 200 % and does not matter.
  4. Split by environment. Stated target: the non-production side must not exceed 25 % of the total.
  5. Untagged cost. It should tend to zero; if it rises, there are new resources created outside the pipeline.
  6. Anomalies detected since the last review and what was done about each one.
  7. Forecast for the current month and comparison with the budget (11-04).
  8. One concrete action with an owner and a date. Only one. A monthly meeting that produces twelve actions a year that get done is worth more than one that produces forty that get abandoned.

Cost anomaly detection

Looking at the bill once a month has an obvious problem: a mistake made on the 6th is discovered on the 5th of the following month, with thirty days of accumulated spend. AWS Cost Anomaly Detection covers that gap with machine learning over the historical spend pattern, and it is free.

The concepts:

  • A monitor defines what is watched: all services, an account, a tag or a cost category.
  • A subscription defines who is notified, with what threshold and how often.
  • Thresholds can be absolute (more than X USD of impact) or percentage-based (more than X % above expected). They can be combined with AND/OR.

MercadoFresco's three monitors:

# 1. General per-service monitor: catches any service that takes off
aws ce create-anomaly-monitor --anomaly-monitor '{
  "MonitorName": "mf-todos-los-servicios",
  "MonitorType": "DIMENSIONAL",
  "MonitorDimension": "SERVICE"
}'

# 2. Monitor specific to the non-production accounts, more sensitive
aws ce create-anomaly-monitor --anomaly-monitor '{
  "MonitorName": "mf-entornos-no-productivos",
  "MonitorType": "CUSTOM",
  "MonitorSpecification": {
    "Dimensions": {
      "Key": "LINKED_ACCOUNT",
      "Values": ["222233334444", "333344445555"],
      "MatchOptions": ["EQUALS"]
    }
  }
}'

# 3. Subscription: notify alertas-mercadofresco when the impact exceeds
#    25 USD absolute OR 40 % above expected
aws ce create-anomaly-subscription --anomaly-subscription '{
  "SubscriptionName": "mf-avisos-coste",
  "MonitorArnList": [
    "arn:aws:ce::999988887777:anomalymonitor/mf-todos-los-servicios",
    "arn:aws:ce::999988887777:anomalymonitor/mf-entornos-no-productivos"
  ],
  "Subscribers": [
    {"Type": "SNS", "Address": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco"}
  ],
  "Frequency": "IMMEDIATE",
  "ThresholdExpression": {
    "Or": [
      {"Dimensions": {"Key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
                      "Values": ["25"], "MatchOptions": ["GREATER_THAN_OR_EQUAL"]}},
      {"Dimensions": {"Key": "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
                      "Values": ["40"], "MatchOptions": ["GREATER_THAN_OR_EQUAL"]}}
    ]
  }
}'

Notes on the decisions taken here:

  • MonitorType: DIMENSIONAL with SERVICE automatically creates a monitor for every service in use. It is the sensible default and it needs no maintenance.
  • The second monitor exists because the non-production environments have a much flatter pattern: any rise is suspicious, whereas in production a rise may simply be a good Friday.
  • Frequency: IMMEDIATE only works with SNS; email supports DAILY or WEEKLY. For immediate notification you have to go via SNS, which is also already integrated with Chatbot and reaches the team channel.
  • Thresholds combined with Or avoid the two classic failures: a percentage-only threshold fires constantly on cheap services, and an absolute-only one fails to spot that a small service has multiplied tenfold.

A calibration tip: start with high thresholds and lower them. An anomaly detector that fires three times a week gets muted within a fortnight and is no use to anyone.

The case of the nightly Redshift load

On 14 August at 09:12, alertas-mercadofresco receives this notification:

Cost anomaly detected
  Service:            Amazon Redshift
  Account:            111122223333
  Total impact:       68.40 USD
  Expected cost:      3.10 USD/day
  Actual cost:        14.50 USD/day
  Detected on:        2026-08-14
  First day affected: 2026-08-08

Sara's investigation follows three steps that serve as a template for any cost anomaly:

graph TD
  A["Anomaly alert<br/>Redshift, +68.40 USD"] --> B["1. Group by USAGE TYPE<br/>in Cost Explorer"]
  B -->|"RPU-Hours"| C["It is compute:<br/>something is running too much"]
  B -->|"Storage"| C2["It is data:<br/>something is piling up"]
  C --> D["2. HOURLY view<br/>of the affected days"]
  D --> E["Pattern: continuous activity<br/>02:00 to 08:00, before 40 min"]
  E --> F["3. CloudTrail + query<br/>logs"]
  F --> G["Cause: 7 August deployment<br/>JOIN with no condition"]
  G --> H["Fix + usage limit<br/>+ lower the monitor threshold"]

The three steps, with their concrete result:

  1. Group by usage type in Cost Explorer, filtered to Redshift and the last 14 days. The result points at RPU-Hours, not storage: the problem is compute, not data.
  2. Hourly view of those days. The pattern is unmistakable: before, a 40-minute peak at 02:00; now, continuous activity from 02:00 until 08:00.
  3. CloudTrail and the query logs. On 7 August a new report was deployed with a query that produces a cartesian product through a JOIN with no condition. The query does not fail: it simply takes six hours, and meanwhile the Serverless workgroup never pauses.

The real cost of the incident: 68.40 USD in seven days, which at that rate would be around 340 USD a month, 15 % of the bill, for a badly written JOIN.

The three actions taken, and none of them is just "fix the query":

Action What it prevents
Fix the JOIN and add the query to the pipeline's tests The same thing being deployed again
Set a usage limit on wg-mercadofresco-analitica: alert at 40 RPU-hours a day and stop at 60 Any future query eating the bill
Lower the anomaly monitor threshold for the production account from 25 to 15 USD Detection in two days instead of seven

The general lesson: a cost anomaly is almost always a technical error, not a money problem. The billing alert was, in this case, the most effective error-detection system the team had.

Reduction recommendations and orphan resources

Cost Explorer includes a Rightsizing recommendations section that analyses CPU and memory usage over the last 14 days and proposes downsizing or shutting down. Its limits are worth knowing: it only covers EC2 (not Fargate, not Lambda, not Aurora) and it assumes the pattern of the last two weeks will repeat.

For MercadoFresco, whose compute is now almost all on Fargate, that section contributes little. What does contribute a lot is hunting orphan resources, which needs no special tooling:

Type of orphan How it is spotted At MercadoFresco Cost
Unattached EBS volumes State available 3 volumes, 100 GiB 8.00 USD/month
Unassociated elastic IP addresses No instance and no interface 2 addresses 7.30 USD/month
Old snapshots Older than the retention policy 4 from RDS, 11 from EBS 6.20 USD/month
Stopped EC2 instances with a volume State stopped but the EBS is still paid for 1 bastion stopped since May 4.80 USD/month
ECR images with no lifecycle policy Growing count per repository 412 images, 42 GB 4.20 USD/month
Load balancers with no healthy targets Empty target group 1 ALB from an April test 17.10 USD/month
Log groups with no retention retentionInDays null 84 groups 34.60 USD/month
VPC endpoints in stopped environments Fixed charge with no traffic 4 in pre-production 32.10 USD/month

They add up to 114.30 USD a month on things nobody uses, that is, 5.1 % of the bill thrown away. And they are, without exception, leftovers from legitimate work: a test that was run, an environment that was brought up, an instance that was stopped instead of terminated.

The script MercadoFresco runs every Monday to find them:

#!/usr/bin/env bash
# Orphan hunter. Reports only: it deletes nothing.
REGION=eu-west-1

echo "== Unattached EBS volumes =="
aws ec2 describe-volumes --region $REGION \
  --filters Name=status,Values=available \
  --query 'Volumes[].{Id:VolumeId,GiB:Size,Created:CreateTime}' --output table

echo "== Unassociated elastic IPs =="
aws ec2 describe-addresses --region $REGION \
  --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table

echo "== Log groups with no retention =="
aws logs describe-log-groups --region $REGION \
  --query 'logGroups[?retentionInDays==null].[logGroupName,storedBytes]' \
  --output table

echo "== Target groups with no healthy targets =="
for tg in $(aws elbv2 describe-target-groups --region $REGION \
              --query 'TargetGroups[].TargetGroupArn' --output text); do
  healthy=$(aws elbv2 describe-target-health --region $REGION --target-group-arn "$tg" \
            --query 'length(TargetHealthDescriptions[?TargetHealth.State==`healthy`])' \
            --output text)
  [ "$healthy" = "0" ] && echo "NO HEALTHY TARGETS: $tg"
done

Two design decisions in this script deserve a comment. It reports only, it never deletes: an available volume may be the copy someone deliberately detached before a migration, and an automatic deletion would destroy data. And it runs weekly, not daily, because orphans appear slowly and a daily report that is almost always empty stops being read.

Compute Optimizer and Trusted Advisor

Three tools overlap here and it is worth knowing which to use for what:

Tool What it analyses Strength Limit
Cost Explorer – Rightsizing EC2 with billing data Integrated with the cost EC2 only, 14 days
AWS Compute Optimizer EC2, Auto Scaling, EBS, Lambda, ECS on Fargate Machine learning over real metrics; covers Fargate and Lambda Needs 14 days of data; better with memory metrics
Trusted Advisor (05-05) Predefined cross-cutting checks Covers cost, security, reliability and quotas The full cost checks require the Business plan

For MercadoFresco, the most useful of the three is Compute Optimizer, precisely because it covers what Cost Explorer cannot see:

  • Lambda: it recommends memory per function. The mercadofresco-generar-miniaturas function is at 1,024 MB and peaks at 340 MB; dropping it to 512 MB saves little money but also reduces the runtime, because CPU scales with memory. Care is needed here: lowering a Lambda's memory can make it slower and more expensive, and that has to be measured with power tuning, not assumed.
  • ECS on Fargate: it validates the percentile-based sizing done in 10-02, and flags that svc-mercadofresco-trabajadores is oversized on memory.
  • EBS: it proposes moving the remaining gp2 volumes to gp3, 20 % cheaper with the same baseline performance.

The division of labour that works: Trusted Advisor for the obvious and cross-cutting, Compute Optimizer for fine sizing, Cost Explorer to understand the money and decide. None of the three replaces the monthly review with human judgement, because none of them knows that a pre-production environment has to resemble production even if it is underutilised.

Spend forecasting and how to read it

Cost Explorer projects future spend with a confidence interval. For MercadoFresco, the mid-month forecast for August was 2,180 USD with an 80 % interval between 2,050 and 2,310; the actual close was 2,237.60 USD, inside the interval.

Three warnings about the forecast, in order of importance:

  1. It extrapolates the past. It does not know that on 1 September the development environment starts shutting down overnight, nor that there is a campaign in December. Any decision already taken invalidates the forecast.
  2. The confidence interval is part of the number. Presenting "2,180 USD" without saying "between 2,050 and 2,310" turns an estimate into a promise, and somebody will treat it as one.
  3. It is very poor at the start of the month. With three days of data, MercadoFresco's forecast swung between 1,900 and 2,600 USD. From about the 10th it settles down.

Its legitimate use is as input to the budgets with a forecasted-cost threshold in 11-04: finding out on the 8th that you are going to blow the budget is far more useful than finding out on the 30th that you did.

Querying Cost Explorer from boto3

Everything above can be automated with the API. The complete example MercadoFresco runs every day:

import boto3
from datetime import date, timedelta

ce = boto3.client("ce", region_name="us-east-1")   # the CE endpoint is global

today = date.today()
start = (today - timedelta(days=32)).replace(day=1)  # full previous month

response = ce.get_cost_and_usage(
    TimePeriod={"Start": start.isoformat(), "End": today.isoformat()},
    Granularity="DAILY",
    Metrics=["UnblendedCost"],
    GroupBy=[
        {"Type": "DIMENSION", "Key": "SERVICE"},
        {"Type": "TAG", "Key": "Componente"},
    ],
    Filter={
        "And": [
            {"Dimensions": {"Key": "RECORD_TYPE",
                            "Values": ["Usage"], "MatchOptions": ["EQUALS"]}},
            {"Dimensions": {"Key": "LINKED_ACCOUNT",
                            "Values": ["111122223333"], "MatchOptions": ["EQUALS"]}},
        ]
    },
)

# Accumulate by service to get the top 10 for the period
by_service = {}
for day in response["ResultsByTime"]:
    for group in day["Groups"]:
        service = group["Keys"][0]
        amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
        by_service[service] = by_service.get(service, 0.0) + amount

print(f"{'Service':<45} {'USD':>10}")
for service, amount in sorted(by_service.items(),
                              key=lambda x: -x[1])[:10]:
    print(f"{service:<45} {amount:>10.2f}")

The details you need for this to work and not turn out expensive:

  • The client is created in us-east-1. The Cost Explorer endpoint is global and lives there; creating it in eu-west-1 produces a connection error that throws people off for a while.
  • RECORD_TYPE = Usage excludes taxes, credits and fees, so that the total matches the service cost and not the total billed.
  • Each call costs 0.01 USD. This script runs once a day: 0.30 USD a month. Run every five minutes from a dashboard it would be 87 USD a month, more than ElastiCache costs in pre-production.
  • GroupBy accepts a maximum of two dimensions. For richer cross-sections, the route is the CUR with Athena from 11-02.
  • The response comes paginated with NextPageToken when there are many groups; in production you have to walk through it, and each page is another billed request.

Publishing daily spend as a business metric

The step that turns the analysis into something people look at without meaning to: bringing spend to the dashboard where the business already lives.

import boto3
from datetime import date, timedelta

ce = boto3.client("ce", region_name="us-east-1")
cw = boto3.client("cloudwatch", region_name="eu-west-1")

day = date.today() - timedelta(days=2)   # 2 days: yesterday is not closed yet
until = day + timedelta(days=1)

data = ce.get_cost_and_usage(
    TimePeriod={"Start": day.isoformat(), "End": until.isoformat()},
    Granularity="DAILY",
    Metrics=["UnblendedCost"],
    GroupBy=[{"Type": "DIMENSION", "Key": "LINKED_ACCOUNT"}],
    Filter={"Dimensions": {"Key": "RECORD_TYPE",
                           "Values": ["Usage"], "MatchOptions": ["EQUALS"]}},
)

ENVIRONMENTS = {
    "111122223333": "produccion",
    "222233334444": "preproduccion",
    "333344445555": "desarrollo",
}

metrics, total = [], 0.0
for group in data["ResultsByTime"][0]["Groups"]:
    account = group["Keys"][0]
    amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
    total += amount
    if account in ENVIRONMENTS:
        metrics.append({
            "MetricName": "CosteDiario",
            "Dimensions": [{"Name": "Entorno", "Value": ENVIRONMENTS[account]}],
            "Value": round(amount, 2),
            "Unit": "None",
        })

metrics.append({"MetricName": "CosteDiario",
                "Dimensions": [{"Name": "Entorno", "Value": "total"}],
                "Value": round(total, 2), "Unit": "None"})

cw.put_metric_data(Namespace="MercadoFresco/Tienda", MetricData=metrics)
print(f"Published the cost for {day}: {total:.2f} USD")

Why this matters more than it looks:

  • It uses the figure from two days ago, not yesterday's, because of the refresh delay. Publishing yesterday's produces a series with false troughs that confuse more than they inform.
  • It publishes one series per environment and one total, which lets you put a chart on the mercadofresco-negocio dashboard with CosteDiario next to PedidosConfirmados. Seeing the two lines together is the most direct way to understand unit cost without calculating anything.
  • It enables CloudWatch alarms on cost, with all the machinery from module 5: thresholds, missing data, actions and suppression. An alarm on CosteDiario in development above 10 USD warns you in hours, not weeks.
  • This script lives in a Lambda with EventBridge Scheduler running at 06:00, and its total cost is pennies.

MercadoFresco's ten optimisations

With all the analysis done, Marta and Luis spend two days executing. These are the ten actions, ordered by saving:

# Optimisation Saving/month Risk Effort
1 Nightly and weekend shutdown of pre-production and development 118.00 USD Low Medium
2 Log retention and dropping the level to INFO in production 74.00 USD Low Low
3 From 2 NATs to 1 in pre-production and development 66.00 USD Low Low
4 Aurora reader auto scaling: from 2 to 1 during off-peak hours 54.00 USD Medium Medium
5 Removing the interface endpoints from pre-production 32.00 USD Low Low
6 S3 lifecycle for mercadofresco-registros-web and backups 38.00 USD Low Low
7 Deleting the orphans: volumes, IPs, snapshots, test ALB 41.00 USD Low Low
8 arm64 on workers and pre-production (the shop already had it) 27.00 USD Low Medium
9 Redshift: lower the base RPU, usage limit and drop the duplicate load 27.00 USD Low Low
10 ECR lifecycle: keep 20 tagged, delete untagged > 14 days 11.00 USD Low Low
Total 488.00 USD

The detail of the first three, which are the ones that contribute most.

1. Nightly shutdown (118.00 USD). Pre-production and development run 24×7 for a team that works from 8 to 19 Monday to Friday. Switched on from 07:30 to 20:30 on working days is 65 of the 168 hours in a week: a 61 % reduction in what can be switched off. With EventBridge Scheduler:

# Scale the development service down to zero tasks every night
aws scheduler create-schedule \
  --name mf-desarrollo-apagar \
  --schedule-expression "cron(30 20 ? * MON-FRI *)" \
  --schedule-expression-timezone "Europe/Madrid" \
  --flexible-time-window '{"Mode":"OFF"}' \
  --target '{
    "Arn": "arn:aws:scheduler:::aws-sdk:ecs:updateService",
    "RoleArn": "arn:aws:iam::333344445555:role/rol-mercadofresco-planificador",
    "Input": "{\"Cluster\":\"ecs-mercadofresco\",\"Service\":\"svc-mercadofresco-tienda-fg\",\"DesiredCount\":0}"
  }'

The explicit Europe/Madrid time zone is no small detail: without it, daylight saving shifts the shutdown by an hour and, twice a year, somebody finds the environment down at 19:30. What gets switched off: Fargate tasks to zero, Aurora instances stopped — remembering that RDS and Aurora start themselves up again after 7 days stopped, so you need a scheduler to stop them again — and the Redshift workgroup, which pauses by itself.

2. Log retention (74.00 USD). Two independent changes. The first, setting retention on the 84 log groups:

# Default policy: 30 days in non-production, 90 in production,
# 365 only for the audit logs
for group in $(aws logs describe-log-groups \
                 --query 'logGroups[?retentionInDays==null].logGroupName' \
                 --output text); do
  case "$group" in
    *cloudtrail*|*auditoria*) days=365 ;;
    *produccion*|*prod*)      days=90  ;;
    *)                        days=30  ;;
  esac
  aws logs put-retention-policy --log-group-name "$group" --retention-in-days $days
  echo "$group -> $days days"
done

The second, dropping the log level from DEBUG to INFO in production, which cuts ingestion from 61 to about 38 GB a month. And an architectural decision taken along the way: the ALB and WAF access logs stop being duplicated in CloudWatch Logs and stay only in S3, where they are already queried with Athena and cost twenty times less.

3. From 2 NATs to 1 (66.00 USD). In pre-production and development one NAT per environment is removed and the traffic from both private subnets is routed through the surviving one. The real consequence, written down so nobody is surprised: if the AZ of the surviving NAT goes down, that environment loses its internet access. In development, that means Luis cannot download dependencies for a while. In production it is left alone, and that asymmetry is exactly what it means to treat environments according to their criticality rather than out of habit.

The overall result:

Item Before After
Monthly bill 2,237.60 USD 1,749.60 USD
Reduction −488.00 USD (−21.8 %)
Annualised saving −5,856 USD
Cost per order 0.01243 USD 0.00972 USD (−21.8 %)
Share of non-production 29.0 % 19.4 %

And an honest observation about this list: none of the ten actions has degraded production. No Aurora reader has been removed at peak, production observability has not been cut below what is needed, and internet egress redundancy has not been touched where there are customers. A 22 % saving without touching service quality is normal on the first optimisation pass over an architecture that has never been reviewed. The second pass costs a great deal more work per dollar.

Common Mistakes and Tips

Mistake: comparing the current month with the full previous month. On the 12th it looks like you have saved 60 %. Tip: compare equivalent periods — day 1 to 12 against day 1 to 12 — or use the forecast, never a partial month against a closed one.

Mistake: drawing conclusions from today's spend. With up to 24 hours of delay, the current day always looks cheap. Tip: work with data from at least two days ago, and publish your metrics with that lag.

Mistake: mixing unblended and amortised cost in the same conversation. Two people see different numbers and argue for half an hour. Tip: always say which metric you are using. Unblended to reconcile with the bill, net amortised to analyse.

Mistake: chasing the biggest line on the bill. Aurora is the largest and it is a conscious, signed-off decision; the badly spent money is in the mid-sized lines nobody decided. Tip: sort by "cost nobody has decided", not by amount.

Mistake: querying the Cost Explorer API from a dashboard that refreshes every minute. At 0.01 USD per request, cost analysis ends up as a line on the bill. Tip: query once a day and publish the result as a CloudWatch metric; the dashboards read the metric, not the API.

Mistake: enabling hourly granularity and forgetting about it. It costs money per record queried and you pay even if you barely use it. Tip: enable it for the specific analysis that needs it — the Friday peak, the stable baseline in 11-05 — and check whether it is still needed.

Mistake: anomaly thresholds that are too sensitive. Three alerts a week and the channel gets muted. Tip: start high — 25 USD or 40 % — and lower them based on real experience, with combined absolute and percentage thresholds.

Mistake: deleting orphans automatically. An available volume may be the copy someone set aside before a migration. Tip: the detector reports, the person decides. And before deleting, tag with FechaBaja and wait a week.

Mistake: shutting environments down without warning. The team finds the environment down at 20:31 in the middle of a test. Tip: announce it, leave a documented manual start-up procedure and begin with development before pre-production.

Tip: the first analysis of a bill that has never been reviewed yields between 15 and 30 %. You do not need to be brilliant, you need to look. The hard part is the second pass.

Tip: turn every finding into a permanent check. Log retention is not a task, it is a Config rule; the ECR lifecycle is not a deletion, it is a policy; the nightly shutdown is not a reminder, it is a scheduler.

Exercises

Exercise 1: interpreting an anomaly

On 3 October, alertas-mercadofresco receives an alert: Amazon S3 in account 111122223333 has gone from 4.20 to 19.80 USD a day since 28 September. Accumulated impact: 78.00 USD.

  1. List four hypotheses ordered from most to least likely.
  2. State which grouping and which filter you would use in Cost Explorer to rule them out.
  3. If the cause turns out to be that access logging was enabled on the mercadofresco-catalogo-fotos bucket and those logs are written to the same bucket, which three actions would you take?

Exercise 2: prioritising optimisations against a time budget

You have a single day of work and this list of possible actions on MercadoFresco's bill:

Action Saving/month Effort Risk
A. Nightly shutdown of non-production 118 USD 6 h Low
B. Log retention 74 USD 1 h Low
C. Migrate the workers to arm64 27 USD 8 h Medium
D. From 2 NATs to 1 in non-production 66 USD 2 h Low
E. Delete orphans 41 USD 2 h Low
F. Buy a 3-year Savings Plan 210 USD 1 h High
  1. Choose what you would do in that day and justify it.
  2. Why is F dangerous right now, if it has the largest saving and the lowest effort?
  3. Calculate the saving achieved and the percentage against the 2,237.60 USD.

Exercise 3: reading a usage type breakdown

Cost Explorer, filtered to Amazon CloudWatch and grouped by usage type, returns this for a month:

Usage type USD
EUW1-DataProcessing-Bytes 118.40
EUW1-TimedStorage-ByteHrs 34.60
EUW1-CW:MetricMonitorUsage 27.00
EUW1-CW:Requests 18.60
EUW1-CW:AlarmMonitorUsage 8.40
EUW1-CW:Dashboards 6.00
EUW1-CW:Canaries 1.90
  1. Translate each line into what it means in practice.
  2. Which would you tackle first and with which concrete action?
  3. Estimate the saving if ingestion drops from 61 to 38 GB a month and retention goes from indefinite to 30/90 days, knowing that storage would settle at around 130 GB.

Solutions

Solution to exercise 1

(1) Four hypotheses, from most to least likely:

  1. A log that writes to S3 has been enabled — bucket access, ALB logs, WAF logs, the CUR or VPC flow logs — and the volume of PUT requests has shot up. It is the most frequent cause of an abrupt jump in S3, because the cost is not in the bytes but in the requests.
  2. A large volume of data has been uploaded: an initial load, a manual backup, a database export.
  3. A process in a loop rewriting the same objects, or versioning enabled with no lifecycle so every old version is kept.
  4. Egress transfer to the internet or to another region, if some external consumer started reading directly from the bucket, bypassing CloudFront.

(2) How to rule them out. Filter: service = Amazon S3, account = 111122223333, last 30 days, daily granularity. Grouping: usage type. That single view discriminates between the four hypotheses at a glance, because the usage types are different: Requests-Tier1 (PUT requests) points to the first or the third; TimedStorage-ByteHrs points to the second; DataTransfer-Out-Bytes points to the fourth. If Requests-Tier1 dominates, a second step with the CUR grouping by line_item_resource_id gives you the exact bucket.

(3) Three actions if it is access logging writing to the same bucket:

  1. Move the log destination to mercadofresco-registros-web, never to the same bucket being logged. Writing access logs into the bucket itself creates a loop: every log write generates an access, which generates a log. It is a classic mistake and it can grow without limit.
  2. Put a lifecycle on those logs: 30 days in Standard, then Glacier Instant Retrieval, deletion at 180. Access logs are almost never consulted after the first month.
  3. Ask whether they are needed at all. If the aim was auditing, CloudTrail data events (05-03) already cover S3 access with a better format and better integration, and they are probably already on. A duplicated log is paid for twice.

Solution to exercise 2

(1) What I would do in one day (8 hours): B (1 h) + D (2 h) + E (2 h) = 5 hours, and with the remaining 3 hours start A, leaving the scheduler written and tested in development but not yet applied to pre-production.

The reasoning: B, D and E are low risk, low effort and immediate effect, and together they come to 181 USD a month for five hours of work. A has the largest saving but needs six hours on the clock plus a heads-up to the team, so splitting it is the sensible move. C waits for another week: eight hours for 27 USD a month is the worst ratio on the list, and it also carries medium risk because it requires testing the arm64 image in pre-production first.

(2) Why F is dangerous. For a reason of sequencing that 11-05 develops and that is worth internalising now: committing capacity before optimising is buying what you are about to stop using. If you buy a three-year Savings Plan sized on current consumption and the following week you carry out actions A to E, consumption drops 22 % and the surplus commitment is still paid for over three years. Two aggravating factors pile on top: three years is a horizon over which the architecture will almost certainly change, and a Savings Plan cannot be cancelled. The right order is always: first switch off what is spare, then size what is left, and only then commit.

(3) Saving achieved:

B (log retention) ................  74.00 USD
D (from 2 NATs to 1) .............  66.00 USD
E (orphans) ......................  41.00 USD
--------------------------------------------
Total ............................ 181.00 USD/month
Over 2,237.60 USD ................ 8.1 %
Annualised ....................... 2,172 USD

Eight per cent of the bill for five hours of work. It is a return no other technical activity matches, and it is exactly why the improvement plan in 11-01 started with the cost pillar.

Solution to exercise 3

(1) Translation of each line:

Usage type What it is in practice
DataProcessing-Bytes Log ingestion: GB arriving at CloudWatch Logs. The dominant line
TimedStorage-ByteHrs Storage of already ingested logs, month by month. It grows if there is no retention
CW:MetricMonitorUsage Custom metrics, at 0.30 USD each per month. There are 90
CW:Requests API calls: PutMetricData, GetMetricData, Container Insights
CW:AlarmMonitorUsage Alarms, at 0.10 USD standard. There are 84
CW:Dashboards Dashboards above the free 3
CW:Canaries Synthetics checks, per run

(2) What to tackle first. The two log lines, which are 71 % of the total, and in this order:

  1. Retention (TimedStorage), because it is the lowest-risk change: one command per group, no effect on the application and the saving starts the following month. All you have to decide with judgement is the audit groups, which must be kept longer.
  2. Ingestion (DataProcessing), dropping the level to INFO in production and stopping the duplication of ALB and WAF logs into CloudWatch when they are already in S3.
  3. Further back, reviewing the 90 custom metrics and the 84 alarms: there are almost always metrics nobody charts and alarms nobody attends to. That is 35 USD a month and, more importantly, every alarm nobody looks at degrades the credibility of the ones that do matter.

(3) Estimated saving:

Ingestion:      61 GB -> 38 GB, at ~0.50 USD/GB
                118.40 -> ~73.70 USD          saving  44.70 USD
Storage:        1.1 TB -> ~130 GB steady state
                 34.60 -> ~ 4.10 USD          saving  30.50 USD
------------------------------------------------------------------
Total estimated saving                                75.20 USD/month

It matches the 74 USD of action 2 on the optimisation list. Two caveats so as not to over-promise: the storage saving is not immediate, because the existing logs expire over the following weeks according to their age, so the full effect shows in the second or third month; and dropping from DEBUG to INFO has a real cost that has to be accepted in writing, which is having less detail available the day you have to investigate an odd incident in production. The mitigation is to leave DEBUG level switchable on demand through a parameter in /mercadofresco/produccion/... with no need to deploy.

Conclusion

MercadoFresco has opened up its bill and come out of it with 22 % less spend and no degradation of service.

You know what Cost Explorer is and what data it works with: 13 months of history, free daily granularity and paid hourly granularity, up to 24 hours of delay — hence the rule of never drawing conclusions from the current day — and a free interface with an API that costs 0.01 USD per request, which is the expensive mistake that turns a cost dashboard into a line on the bill.

You have the billing concepts you need to understand before looking at a chart: unblended to reconcile with the bill, amortised to analyse once there are commitments — with the example of the 1,200 USD Savings Plan that shows up as a March spike or as 100 USD a month depending on the metric — and the net variants that deduct credits. Plus the reason the total never matches what was charged: the Tax, Credit and Refund line types and the fees, with the rule of always working on the service cost, which is the only one engineering can optimise. And consolidated billing across the six accounts, with its three consequences: a single bill, volume tier aggregation and shared commitments.

You have MercadoFresco's complete bill: 27 lines up to 2,237.60 USD, headed by Aurora (342.60), Fargate (262.40), NAT Gateway (243.80) and CloudWatch (214.90). And the right way to read it, which is not from largest to smallest amount but by separating what was decided from what was not: Aurora is there because of a signed ADR, whereas the 611.20 USD of NAT, cross-AZ transfer, CloudWatch and residual EC2 — 27.3 % of the bill — were never decided by anyone.

You have the three surprises taken apart with their specific origin. The NAT Gateway, 81 % of which is the fixed hourly charge and not traffic, with 143.80 USD a month paid by environments without a single customer. Cross-AZ transfer, the most invisible cost in AWS because there is no resource to blame, which comes from the ALB spreading traffic across AZs, from Aurora's replication and from ElastiCache's, and which must not be removed because it is the price of high availability. And CloudWatch, where 71 % is logs and the root cause is the same in both lines: nobody ever decided what to log or how long to keep it.

You have the monitoring tools: filters and groupings — with usage type as the most underrated dimension, the one that turns "VPC: 362.40 USD" into four actionable lines — the five saved reports, the eight-step script for Marta's monthly review that always ends with a single action with an owner and a date, and anomaly detection with dimensional monitors, combined absolute and percentage thresholds, and immediate notification over SNS. Along with the real case of the nightly Redshift load — a JOIN with no condition that cost 340 USD a month and was caught by the bill — and its lesson: a cost anomaly is almost always a technical error, not a money problem.

You have the hunt for orphan resources, 114.30 USD a month on things nobody uses, with the weekly script that reports and never deletes; the division of labour between Cost Explorer, Compute Optimizer and Trusted Advisor, knowing that only Compute Optimizer sees Fargate and Lambda; the forecast with its three warnings, starting with the fact that it extrapolates the past and therefore any decision already taken invalidates it; and the query from boto3 with get_cost_and_usage, the client in us-east-1, the RECORD_TYPE=Usage filter and the script that publishes CosteDiario in MercadoFresco/Tienda with a two-day lag, so that spend finally lives on the same dashboard as the orders.

And you have the ten optimisations carried out with their savings: nightly shutdown (118), log retention (74), from two NATs to one in non-production (66), Aurora reader auto scaling (54), S3 lifecycle (38), orphans (41), pre-production endpoints (32), arm64 on the workers (27), Redshift limits (27) and ECR lifecycle (11). 488.00 USD a month, 21.8 %, which leaves the bill at 1,749.60 USD and the cost per order at 0.00972 USD. With the honest observation that none of the ten has degraded production, and with the warning about what comes next: the first optimisation pass over an architecture that has never been reviewed yields between 15 and 30 % without any brilliance; the second costs a great deal more work per dollar.

There is one problem this analysis does not solve. Everything above is looking backwards: you discover what has already been spent. The anomaly detector warns you within a day or two, which is fine, but nobody has yet put a limit in place. If tomorrow Luis brings up a test EKS cluster in the development account and forgets about it, or if a new query takes off on a Friday night, the bill will grow with nothing standing in its way. And the 10 USD presupuesto-mensual-mercadofresco created in 01-02 has been firing on the 2nd of every month for twenty-odd months, to the point where nobody reads its emails any more.

In 11-04, "AWS Budgets", you move from analysing to controlling: budgets by account, by service, by tag and by cost category, with thresholds on actual and forecasted cost, with escalating notifications and with automatic actions capable of freezing the development account when it reaches its limit. Plus the FinOps routine that sustains the whole cycle.

© Copyright 2026. All rights reserved