The previous lesson left MercadoFresco's bill at 1,749.60 USD a month, 22 % below the starting point, and with an analysis capable of explaining every line. But all of that is looking backwards. If tomorrow someone brings up a test 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. Nobody has yet put a limit in place.

This lesson moves from analysing to controlling. You will see the difference between Cost Explorer and Budgets and why the billing alarm from 01-02 falls short, the four budget types and their periodicities — including planned budgets for the Christmas campaign — the distinction between thresholds on actual and forecasted cost, the specific budgets MercadoFresco creates by account, service, tag and cost category, how to create them from the console, the CLI and the CDK, the notifications, and the part with teeth: the budget actions capable of freezing the development account when it reaches its limit. At the end, the FinOps routine that sustains the whole cycle.

Cost warning. The first two budgets are free; beyond that they cost 0.02 USD per budget per day, that is, around 0.60 USD a month each. MercadoFresco's ten budgets cost roughly 4.80 USD a month, 0.27 % of the bill, which is probably the best insurance in the catalogue. Budget actions cost extra, around 0.10 USD per action per day. Fictitious data, accounts and identifiers.

Contents

  1. Analysing versus controlling
  2. Why the billing alarm from 01-02 falls short
  3. The four budget types
  4. Periodicity: fixed, recurring and planned
  5. Actual cost versus forecasted cost
  6. Staggered thresholds and who each one notifies
  7. MercadoFresco's budgets
  8. Creating a budget from the console, the CLI and the CDK
  9. The complete JSON, annotated
  10. Notifications: email, SNS and Slack
  11. Budget actions
  12. The development account that freezes itself
  13. The warning about production
  14. Budget reports and the monthly review
  15. Good practices
  16. When a budget is exceeded for a legitimate reason
  17. FinOps: inform, optimise and operate
  18. The monthly cost meeting
  19. Common mistakes and tips
  20. Exercises
  21. Conclusion

Analysing versus controlling

The two tools look alike on screen and are radically different in purpose:

Cost Explorer (11-03) AWS Budgets
Question it answers What have we spent on? Are we going to overshoot?
Direction in time Backwards Forwards
Use One-off investigation and monthly review Continuous unattended monitoring
Output Charts and tables for a person Notifications and actions
How often you look When someone goes in Never: it tells you
Can it prevent spend No Yes, with budget actions

The practical difference is simple: Cost Explorer requires someone to remember to look; Budgets requires nobody to do anything. And in a three-person company, anything that depends on someone remembering ends up failing some month.

It is also worth placing it against the anomaly detection from 11-03, which people confuse it with:

  • Anomaly detection: "this spend does not look like your usual pattern". It is statistics, and it has no opinion on whether you can afford it.
  • Budgets: "you have gone past the limit you set yourself". It is a business decision expressed as a number.

Both are necessary and they answer different cases. A gradual 3 % monthly rise over six months does not trigger any anomaly — it is perfectly normal month to month — and it does end up blowing the budget. A one-day spike triggers the anomaly and probably barely moves the monthly budget.

Why the billing alarm from 01-02 falls short

In lesson 01-02, with the account freshly created, a CloudWatch billing alarm was configured on the EstimatedCharges metric with a threshold of 10 USD, along with the presupuesto-mensual-mercadofresco budget for the same amount. That was the right thing then and today it is useless, for five reasons:

  1. The amount is obsolete. It has been firing on the 2nd of every month for twenty-odd months. An alarm that is always red is not an alarm: it is noise, and nobody reads its emails any more.
  2. It is global. A single number for the whole organisation does not tell you where the problem is.
  3. It only looks at actual cost. It finds out once the money is spent, not when it is about to be spent.
  4. The EstimatedCharges metric only exists in us-east-1 and belongs to the management account: it cannot watch a member account separately.
  5. It cannot do anything. It notifies and that is all.

The first action in this lesson is therefore to retire that budget: raise its amount to the real value and turn it into the organisation-wide budget. It is not deleted — it keeps its history — but it stops being a relic.

The four budget types

Type What it measures Example at MercadoFresco
Cost (COST) Money spent in a period "Development must not go above 170 USD a month"
Usage (USAGE) Quantity of a specific usage type "No more than 900 GB processed by the NATs a month"
Savings Plans (SAVINGS_PLANS_UTILIZATION / _COVERAGE) What percentage of the commitment is used and what percentage of usage is covered "Warn me if utilisation drops below 95 %"
Reservations (RI_UTILIZATION / RI_COVERAGE) The same for reserved instances "Warn me if ElastiCache coverage drops below 80 %"

The first two are the ones used day to day. The last two exist because a poorly used commitment is money lost silently, and they become essential from 11-05 onwards.

The usage budget is the least known and it solves a problem the cost budget cannot see: if a service's price goes down, a cost budget stops warning you even though consumption has shot up. MercadoFresco uses it for the NAT Gateways, where the volume of data processed is an indicator of architectural health as well as a cost.

Periodicity: fixed, recurring and planned

Periodicity Behaviour When to use it
Monthly recurring The same amount every month, counter reset on the 1st The normal case; 8 of MercadoFresco's 10
Quarterly / annual Accumulates across the whole period Project or financial-year budgets
Fixed (FIXED) A total amount for an interval with an end date A migration, a proof of concept, a contract
Planned (PLANNED) Different amounts per month, defined in advance Known seasonality

The planned budget is what solves MercadoFresco's real problem: in December, the Christmas campaign multiplies orders and with them the spend. With a monthly recurring budget of 2,000 USD, December trips every threshold and the team learns to ignore them in exactly the month when attention matters most.

MercadoFresco's planned budget:

Month Amount Reason
January to October 2,000 USD Normal operation
November 2,300 USD Campaign preparation, load testing
December 2,600 USD Campaign: ×1.6 orders in the first three weeks
The following January 2,100 USD Campaign tail and returns

And with this you gain something more valuable than a well-calibrated alert: the November conversation. Putting a number on December forces you to estimate it, and estimating it forces you to talk to the business about how many orders are expected. A budget is, before it is a control, an exercise in forecasting.

Actual cost versus forecasted cost

Each threshold in a budget can be evaluated against two different things, and you have to configure both:

Threshold type When it fires What it gives you
Actual cost (ACTUAL) When what has been spent reaches the threshold Certainty: it has already happened
Forecasted cost (FORECASTED) When the end-of-month projection reaches the threshold Anticipation: it can still be avoided

The example that makes it clear. A development budget of 170 USD a month:

  • On the 9th, 51 USD has been spent. The actual 80 % threshold (136 USD) does not fire: there is a long way to go.
  • That same day, the end-of-month forecast is 170 USD, because the daily rate has been climbing since the 6th. The forecasted 100 % threshold does fire.
  • There are 21 days left to correct it. That is the whole difference between finding out in time and finding out too late.

Two warnings about the forecast, which is not magic:

  1. It needs history. A freshly created budget, or a new account, does not produce reliable forecasts for the first few weeks. AWS needs on the order of five weeks of data.
  2. It extrapolates. If a one-off migration on the 3rd consumed a lot, the forecast will believe that repeats all month and will fire a false alarm. You learn to recognise them.

MercadoFresco's standard configuration combines both: forecasted at 80 % and at 100 % to anticipate, actual at 100 % to confirm, and in the case of development, actual at 100 % with an automatic action.

Staggered thresholds and who each one notifies

A budget with a single threshold at 100 % warns you when there is nothing left to do. One with five thresholds generates so much noise that all of them get ignored. Three is the number that works:

Threshold Type Meaning Who receives it What is expected
50 % Actual We are halfway through at mid-month: normal Nobody by email; the dashboard only Nothing
80 % Forecasted We are going to graze the limit Marta, by SNS to alertas-mercadofresco Look at the breakdown this week
100 % Forecasted We are going to exceed it Marta + the account owner Decide: fix it or raise the budget
100 % Actual We have already exceeded it Marta + the manager Written explanation in the monthly review
120 % Actual It has got out of hand Everyone, and an action in development Immediate intervention

The logic of the escalation rests on a principle: each threshold must have a different recipient and a different expected action. If the 50 % and the 80 % notify the same people to do the same thing, one of the two is redundant. And MercadoFresco's 50 % deliberately emails nobody: reaching half the budget at mid-month is exactly what should happen.

MercadoFresco's budgets

Ten budgets, all consistent with the 1,749.60 USD bill that followed the optimisations in 11-03:

Budget Scope Current spend Amount Thresholds Action
presupuesto-mensual-mercadofresco The whole organisation 1,749.60 USD 2,000 USD 80 % and 100 % forecasted, 100 % actual No
pres-mf-produccion Account 111122223333 1,266.10 USD 1,400 USD 80 % and 100 % forecasted, 100 % actual No, never
pres-mf-preproduccion Account 222233334444 245.00 USD 290 USD 80 % and 100 % forecasted No
pres-mf-desarrollo Account 333344445555 143.30 USD 170 USD 80 % forecasted, 100 % actual Yes: freeze
pres-mf-herramientas Account 555566667777 51.40 USD 70 USD 100 % forecasted No
pres-mf-gobierno Accounts 4444… and 9999… 43.80 USD 55 USD 100 % forecasted No
pres-mf-analitica Tag Componente=analitica 187.50 USD 220 USD 80 % and 100 % forecasted No
pres-mf-cloudwatch CloudWatch service, all accounts 140.90 USD 170 USD 100 % forecasted No
pres-mf-nat-uso Usage: GB processed by the NATs 712 GB 900 GB 90 % actual No
pres-mf-navidad Organisation, planned 2,000-2,600 USD 100 % forecasted No

Four design decisions in this table deserve explaining:

The margin over current spend is between 10 and 20 %. Neither tight — it would fire every month on normal variation — nor generous — it would never warn you. MercadoFresco's rule: the budget is set 15 % above the actual spend of the last three months, and it is reviewed every quarter.

The sum of the per-account budgets (1,985 USD) is lower than the global one (2,000 USD). That is deliberate: if every environment approaches its limit at once, the global one also warns. The other way round — per-team budgets that add up to more than the global one — is the classic mistake that means nobody ever feels addressed.

Development is the strictest and the only one with an action. Its margin is 18 % but its action threshold is actual cost at 100 %, not forecasted, so as not to freeze the account on a wrong forecast. It is the environment where a mistake costs little to fix and where most experiments happen.

Some budgets overlap on purpose. The analitica one cuts across accounts and the CloudWatch one cuts across services: both capture spend that is already counted in the per-account budgets. There is nothing wrong with counting twice when the purpose is to watch two different dimensions. What you must not do is add them together.

Creating a budget from the console, the CLI and the CDK

From the console, under Billing → Budgets → Create budget, the flow is: template or custom → type → periodicity and amount → filters (account, service, tag, category) → thresholds and recipients → actions. It is convenient for the first one and inadvisable for all ten: a budget created by hand is not in Git, is not reviewed by pull request and disappears if someone deletes it.

From the CLI, with two JSON files:

aws budgets create-budget \
  --account-id 999988887777 \
  --budget file://pres-mf-desarrollo.json \
  --notifications-with-subscribers file://pres-mf-desarrollo-avisos.json

With the CDK, which is how MercadoFresco really maintains them, in the mercadofresco-infra repository:

from aws_cdk import Stack, aws_budgets as budgets
from constructs import Construct

class BudgetsStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        ALERTS_TOPIC = "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco"

        def budget(name, amount, account=None, tag=None,
                   service=None, with_action=False):
            filters = {}
            if account:
                filters["LinkedAccount"] = [account]
            if tag:
                filters["TagKeyValue"] = [f"user:Componente${tag}"]
            if service:
                filters["Service"] = [service]

            notifications = [
                # 80 % forecasted: early warning
                budgets.CfnBudget.NotificationWithSubscribersProperty(
                    notification=budgets.CfnBudget.NotificationProperty(
                        comparison_operator="GREATER_THAN",
                        notification_type="FORECASTED",
                        threshold=80, threshold_type="PERCENTAGE"),
                    subscribers=[budgets.CfnBudget.SubscriberProperty(
                        address=ALERTS_TOPIC, subscription_type="SNS")]),
                # 100 % actual: confirmation
                budgets.CfnBudget.NotificationWithSubscribersProperty(
                    notification=budgets.CfnBudget.NotificationProperty(
                        comparison_operator="GREATER_THAN",
                        notification_type="ACTUAL",
                        threshold=100, threshold_type="PERCENTAGE"),
                    subscribers=[budgets.CfnBudget.SubscriberProperty(
                        address=ALERTS_TOPIC, subscription_type="SNS")]),
            ]

            return budgets.CfnBudget(
                self, name,
                budget=budgets.CfnBudget.BudgetDataProperty(
                    budget_name=name,
                    budget_type="COST",
                    time_unit="MONTHLY",
                    budget_limit=budgets.CfnBudget.SpendProperty(
                        amount=amount, unit="USD"),
                    cost_filters=filters or None,
                    cost_types=budgets.CfnBudget.CostTypesProperty(
                        include_tax=False,          # taxes cannot be optimised
                        include_credit=False,       # credits mask the spend
                        include_refund=False,
                        use_amortized=True,         # consistent with 11-03
                    ),
                ),
                notifications_with_subscribers=notifications,
            )

        budget("presupuesto-mensual-mercadofresco", 2000)
        budget("pres-mf-produccion",    1400, account="111122223333")
        budget("pres-mf-preproduccion",  290, account="222233334444")
        budget("pres-mf-desarrollo",     170, account="333344445555",
               with_action=True)
        budget("pres-mf-herramientas",    70, account="555566667777")
        budget("pres-mf-analitica",      220, tag="analitica")
        budget("pres-mf-cloudwatch",     170, service="AmazonCloudWatch")

Three details in the code that head off frequent errors:

  • TagKeyValue with the format user:Key$Value. That $ as a separator and the user: prefix are mandatory and they do not appear in the most obvious place in the documentation. With the syntax written wrongly, the budget is created without error and filters down to zero, so it never warns you.
  • include_tax=False and include_credit=False. Consistent with the decision in 11-03: the working number is the service cost. With taxes included, a 2,000 USD budget would fire at 1,653 USD of actual spend and nobody would know why.
  • use_amortized=True. Today it changes nothing because there are no commitments, but from 11-05 onwards it does: without amortising, the month a Savings Plan is paid upfront would trip every threshold at once.

The complete JSON, annotated

The CLI equivalent of the development budget, which is the most complete because it includes an action:

{
  "BudgetName": "pres-mf-desarrollo",
  "BudgetType": "COST",
  "TimeUnit": "MONTHLY",
  "BudgetLimit": { "Amount": "170", "Unit": "USD" },
  "CostFilters": {
    "LinkedAccount": ["333344445555"]
  },
  "CostTypes": {
    "IncludeTax": false,
    "IncludeSubscription": true,
    "IncludeRefund": false,
    "IncludeCredit": false,
    "IncludeUpfront": true,
    "IncludeRecurring": true,
    "IncludeOtherSubscription": true,
    "IncludeSupport": true,
    "IncludeDiscount": true,
    "UseAmortized": true,
    "UseBlended": false
  }
}

And the notifications file, which is a separate file and not a property of the previous one:

[
  {
    "Notification": {
      "NotificationType": "FORECASTED",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 80,
      "ThresholdType": "PERCENTAGE",
      "NotificationState": "ALARM"
    },
    "Subscribers": [
      { "SubscriptionType": "SNS",
        "Address": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco" },
      { "SubscriptionType": "EMAIL", "Address": "luis@mercadofresco.example" }
    ]
  },
  {
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 100,
      "ThresholdType": "PERCENTAGE"
    },
    "Subscribers": [
      { "SubscriptionType": "SNS",
        "Address": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco" },
      { "SubscriptionType": "EMAIL", "Address": "marta@mercadofresco.example" }
    ]
  }
]

The fields that matter:

  • ThresholdType accepts PERCENTAGE or ABSOLUTE_VALUE. The percentage survives changes to the budget amount; the absolute one has to be updated by hand and gets forgotten.
  • ComparisonOperator accepts GREATER_THAN, LESS_THAN and EQUAL_TO. The LESS_THAN has a little-known legitimate use: warning that the utilisation of a Savings Plan has fallen below a minimum, which is exactly what will be needed in 11-05.
  • IncludeSupport: true and IncludeSubscription: true are left on: the support fee and the subscriptions are real spend that has to be controlled, unlike taxes.
  • You can mix SNS and email recipients in the same threshold. Ten subscribers maximum per notification.

Notifications: email, SNS and Slack

Three channels, with different roles:

Channel Advantage Drawback Use at MercadoFresco
Email Needs no configuration Gets lost among the rest; nobody reads it on a Friday 100 % actual thresholds, as a record
SNS Integrates with everything: Lambda, queues, Chatbot Requires a topic policy The main channel
Chatbot → Slack/Teams Arrives where the team is already looking One more channel that can be muted 80 % and 100 % forecasted thresholds

For Budgets to be able to publish to the alertas-mercadofresco topic you have to authorise it explicitly in the topic policy, and it is the step most often forgotten:

{
  "Sid": "PermitirPublicarABudgets",
  "Effect": "Allow",
  "Principal": { "Service": "budgets.amazonaws.com" },
  "Action": "SNS:Publish",
  "Resource": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
  "Condition": {
    "StringEquals": { "aws:SourceAccount": "999988887777" },
    "ArnLike": { "aws:SourceArn": "arn:aws:budgets::999988887777:budget/*" }
  }
}

The aws:SourceAccount and aws:SourceArn conditions are not decoration: without them, any budget in any AWS account could publish to that topic. It is the confused deputy protection pattern already seen in 04-01.

The result in Slack, through the #mercadofresco-alertas channel the team has used since module 5, has an added advantage over email: it is public within the team. An alert everyone can see gets dealt with; one that arrives in a shared inbox does not.

Budget actions

Here Budgets stops being an alerting system and becomes a control. A budget action runs when a threshold is reached and can do three things:

Action What it does Reversible
Apply an IAM policy Attaches a policy — usually restrictive — to users, groups or roles Yes, by removing it
Apply a service control policy (SCP) Attaches an SCP to an OU or account Yes
Stop instances Stops specific EC2 instances or RDS clusters Yes, by starting them

And in two modes:

  • Automatic (AUTOMATIC): it runs on its own when the threshold is reached.
  • Manual (MANUAL): it notifies and waits for an authorised person to approve it from the console.

The rule that governs the choice is simple and admits no convenient exceptions: automatic only where the worst case is an inconvenience; manual where the worst case is an incident.

The development account that freezes itself

MercadoFresco's specific case: account 333344445555 has a 170 USD budget and, on reaching 100 % of actual cost, an SCP that prevents creating new resources is attached to it automatically.

The policy that is applied:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CongelarCreacionDeRecursosCaros",
      "Effect": "Deny",
      "Action": [
        "ec2:RunInstances",
        "ec2:CreateVolume",
        "rds:CreateDBInstance",
        "rds:CreateDBCluster",
        "eks:CreateCluster",
        "elasticache:CreateReplicationGroup",
        "redshift-serverless:CreateWorkgroup",
        "ecs:CreateService",
        "elasticloadbalancing:CreateLoadBalancer",
        "sagemaker:CreateNotebookInstance"
      ],
      "Resource": "*"
    }
  ]
}

And its configuration as an action:

aws budgets create-budget-action \
  --account-id 999988887777 \
  --budget-name pres-mf-desarrollo \
  --notification-type ACTUAL \
  --action-type SCP \
  --action-threshold '{"ActionThresholdValue": 100, "ActionThresholdType": "PERCENTAGE"}' \
  --approval-model AUTOMATIC \
  --execution-role-arn arn:aws:iam::999988887777:role/rol-mercadofresco-acciones-presupuesto \
  --definition '{
    "ScpActionDefinition": {
      "PolicyId": "p-mfcongelar01",
      "TargetIds": ["333344445555"]
    }
  }' \
  --subscribers '[
    {"SubscriptionType": "SNS",
     "Address": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco"}
  ]'

The decisions that make this safe rather than a bomb:

  • Only the creation of expensive resources is denied. What is already running keeps running: nobody loses work in progress. You can still deploy code, read logs and query databases. The only thing you cannot do is switch on something new and expensive.
  • It fires on actual cost, not forecasted. Freezing an account on a wrong forecast would be unacceptable even in development.
  • The SCP is applied only to account 333344445555, not to the whole OU. A mistake in TargetIds pointing at the Cargas OU would freeze production too, and that is exactly the kind of accident you have to design out of existence.
  • The execution role has minimum permissions: only organizations:AttachPolicy on that policy and that account.
  • There is a written unblocking procedure in mercadofresco-infra/docs/runbooks/, saying who can run it (Marta) and what gets documented afterwards. An automatic action with no documented reversal procedure is a trap for the team.

The real effect observed in the first month: on 24 September, the account reached 170 USD because Luis had brought up an EKS cluster for the exercise in 10-03 and left it on over a weekend. The SCP was applied, Luis saw the alert in Slack, deleted the cluster and Marta removed the SCP within ten minutes. Cost of the incident: about 40 USD. Without the budget, the cluster would have stayed on until the monthly review: about 220 USD.

The warning about production

This needs saying with complete clarity because it is the gravest mistake you can make with this tool:

Never apply a restrictive automatic action to the production account without understanding exactly what it breaks.

The scenario that explains it: on a campaign Friday, orders take off, spend rises and the production budget hits 100 %. The automatic action denies ecs:CreateService and elasticloadbalancing:CreateLoadBalancer. So far, perhaps harmless. But if the policy included ec2:RunInstances or any action used by auto scaling, the system could not scale precisely at the moment it is most needed, and a decision to save 300 USD would cause an outage costing far more than that.

That is why MercadoFresco's pres-mf-produccion budget:

  • Has no automatic action. None.
  • Has a manual action of reinforced notification only at 120 %, which requires explicit approval and which in practice is a reminder that someone needs to look.
  • Its margin is larger than the rest, because a good sales month must fit inside it without raising an alarm.

And the general principle that follows: cost control must never be able to degrade the service to customers. An exceeded budget is fixed with a decision, not with an automatic cut-off.

Budget reports and the monthly review

AWS Budgets Reports lets you schedule a periodic report — daily, weekly or monthly — with the status of up to 50 budgets, emailed to a maximum of 50 recipients. It costs 0.01 USD per report sent.

MercadoFresco has one:

Parameter Value
Name informe-mensual-presupuestos-mf
Frequency Monthly, on the 3rd
Budgets included All ten
Recipients Marta, Luis, Sara and the manager
Cost 0.01 USD a month

Its purpose is not to inform — the team already gets alerts in Slack — but to open the monthly meeting with a common document. Having all four people look at the same PDF before sitting down saves the first fifteen minutes of any cost meeting.

Good practices

The seven rules MercadoFresco writes into its cost management document:

  1. One budget per unit with an owner, not just a global one. A global budget tells you there is a problem; one per account or per component tells you where it is. The global one without the specific ones is nearly useless.
  2. Margins of 10 to 20 % over actual spend in the last three months. Tight generates false alarms; generous never warns you.
  3. Quarterly review of every amount, coinciding with the review of a Well-Architected pillar. A budget that has not been touched for a year is wrong, whether it is too high or too low.
  4. Forecasted thresholds to act, actual ones to confirm. Always both.
  5. Everything in code. The ten budgets live in mercadofresco-infra and are deployed by the pipeline. One created by hand in the console vanishes without trace when someone deletes it.
  6. Automatic actions only where the worst case is an inconvenience. Development yes, pre-production maybe, production never.
  7. An exceeded budget always generates a written line, even if the conclusion is "it is normal, we are raising the amount". Without that record, in six months nobody will remember why the budget is what it is.

When a budget is exceeded for a legitimate reason

This is the most common case and the worst handled. The business grows, orders rise 35 %, the bill rises 20 % and the budget trips. There is no error, no forgotten resource and no inefficiency: the number was simply out of date.

MercadoFresco's procedure, in four steps:

  1. Check the unit cost before the total. If the cost per order has fallen or held steady, the growth is healthy and the conversation is a different one. If it has risen, there is something more than growth going on.
  2. Identify the specific cause. "Orders are up" is not enough; you need to see which lines of the bill have grown and check that the ones growing are the ones that should — Fargate, Aurora, queues — and not the ones that should not — logs, transfer, orphans.
  3. Decide explicitly: raise the budget, optimise, or both. The decision is taken by whoever owns the budget, not by whoever watches it.
  4. Document the change of amount with its date and its reason in the same repository where the budget lives. The commit is the record.

What you must not do, and is done constantly: raise the amount in the console without telling anyone. A year later, nobody knows why the production budget is 3,400 USD or who decided it, and the number has stopped meaning anything.

FinOps: inform, optimise and operate

Everything covered in the last three lessons has a name in the industry: FinOps, the discipline of managing cloud spend as a shared responsibility between technology, finance and the business. Its model has three phases that repeat in a cycle:

graph LR
  I["INFORM<br/>Visibility and allocation<br/>11-02 and 11-03"] --> O["OPTIMISE<br/>Reduce and commit<br/>11-03 and 11-05"]
  O --> P["OPERATE<br/>Govern and automate<br/>11-04"]
  P --> I
Phase What happens Where it was seen Status at MercadoFresco
Inform Tagging, allocation, unit cost, dashboards 11-02, 11-03 Done
Optimise Switch off what is spare, size, commit 11-03, and 11-05 First pass done
Operate Budgets, policies, actions, routine 11-04 Under way

The three principles that hold the model up and are worth keeping in sight:

  • Teams own their spend. There is no one person controlling everybody's money: there is distributed information and local responsibility. That is why the split by Propietario in 11-02 matters.
  • Decisions are taken on business value, not on absolute cost. Spending more can be the right decision. The question is never "how do we spend less?" but "are we getting value for what we spend?".
  • A central team enables, it does not control. At MercadoFresco that team is Marta with two hours a month. In a large company it would be a team, but its role is the same: providing tools and context, not approving spending.

Who takes part, in a three-person company:

Person FinOps role Concrete responsibility
Marta FinOps practitioner and technical lead Maintains budgets and tags; convenes the meeting; decides the technical optimisations
Luis Engineer, owner of his spend Answers for the cost of the shop and of the non-production accounts
Sara Business and data Provides the order volume for the unit cost; answers for analitica
Manager Budget and priority Approves amounts, commitments and the acceptable level of risk

The monthly cost meeting

Marta establishes a 30-minute meeting on the 5th of each month, with a fixed script:

Minutes Content Who
0-3 Cost per order for the month and its variation Marta
3-8 Total and split by environment against the budget Marta
8-15 The three lines that have risen most and why Luis and Sara
15-20 Anomalies and budgets exceeded since the last meeting Marta
20-25 One action for the month, with an owner and a date Everyone
25-30 Forecast for the current month and warnings for the next Marta

The three rules that let the meeting survive more than three months:

  1. Start with the unit cost, not the total. It changes the tone of the conversation from "we spend a lot" to "we spend well or badly".
  2. One action a month. Twelve actions a year that get done are worth more than forty that get abandoned.
  3. Nobody gets a surprise in public. If someone's spend has shot up, it gets discussed beforehand. A cost meeting that turns into a tribunal stops being held.

Common Mistakes and Tips

Mistake: a single global budget. It tells you there is a problem and not where. Tip: one per account as a minimum, and one per critical component. The global one is the ceiling, not the instrument.

Mistake: per-team budgets that add up to more than the global one. Each one thinks it is doing fine and the total takes off without anybody feeling addressed. Tip: the sum of the specific ones should stay below the global one, leaving margin.

Mistake: using only actual cost thresholds. You find out when there is nothing left to do. Tip: forecasted at 80 % and at 100 % to anticipate, actual at 100 % to confirm.

Mistake: leaving the 10 USD budget from the first lesson. It fires on the 2nd of every month, it gets ignored, and with it every other alert on the same channel gets ignored. Tip: a budget that is always red does active harm. Update it or remove it.

Mistake: including taxes and credits in the amount. The budget fires at a much lower actual spend and nobody understands why. Tip: IncludeTax=false and IncludeCredit=false, consistent with the analysis in 11-03.

Mistake: writing the tag filter wrongly. The format is user:Key$Value; with any other syntax the budget is created without error and filters down to zero, so it never warns you. Tip: after creating it, check in the console that it shows a current spend other than zero.

Mistake: an automatic action in production. On a campaign Friday, the policy denies scaling exactly when it is needed. Tip: production never; and if it is ever done, make sure the policy touches nothing that auto scaling uses.

Mistake: forgetting the SNS topic policy. The budget is created, the thresholds are reached and nothing arrives. Tip: add the permission for budgets.amazonaws.com with aws:SourceAccount, and test the alert by temporarily creating a 0.01 USD budget.

Mistake: raising the amount in the console when it fires. Six months later nobody knows why the budget is what it is. Tip: the amount lives in the CDK; changing it is a commit with its reason.

Tip: create the budget before creating the resources. A budget is an agreed limit, not an after-the-fact summary. When a new environment is opened, the first thing deployed is its budget.

Tip: use a usage budget for whatever you do not want to grow, even if it is cheap today: GB through the NAT, GB ingested into CloudWatch, requests to the Cost Explorer API. The price can come down; runaway consumption is still a symptom.

Exercises

Exercise 1: designing the set of budgets

A fictitious company, RopaCircular, sells second-hand clothing. It has three accounts — production (2,800 USD/month), pre-production (600 USD/month) and data (900 USD/month) — and a team of eight people. In November, Black Friday triples its sales. The manager has asked for "things not to get out of hand" and has given an annual ceiling.

  1. Propose the set of budgets with their amounts, types and periodicities.
  2. State which thresholds you would put on each one and who they would notify.
  3. In which account would you put an automatic action and with exactly which policy?
  4. How would you handle November?

Exercise 2: interpreting an alert

On 11 October this alert arrives at alertas-mercadofresco:

AWS Budgets: pres-mf-produccion
  Threshold:       100 % (FORECASTED)
  Budget:          1,400.00 USD
  Current spend:   612.40 USD
  Forecast:        1,482.00 USD
  1. Is it cause for alarm? Reason it out with the data available.
  2. List three checks you would make, in order.
  3. If it turns out that October's orders are running 22 % above September's, what decision would you take and what would you document?

Exercise 3: designing a safe budget action

You are asked to apply an automatic action to the pre-production account 222233334444, with a budget of 290 USD, to stop it taking off as development once did.

  1. Write the policy you would apply, justifying what you include and what you leave out.
  2. Choose between an actual or forecasted threshold and between automatic or manual mode, with justification.
  3. List three things that could go wrong and how you would mitigate them.

Solutions

Solution to exercise 1

(1) Proposed set of budgets:

Budget Scope Type Periodicity Amount
rc-global Organisation Cost Planned 4,800 USD (Nov: 8,500)
rc-produccion Production account Cost Planned 3,200 USD (Nov: 6,500)
rc-preproduccion Pre-production account Cost Monthly 700 USD
rc-datos Data account Cost Monthly 1,050 USD
rc-anual Organisation Cost Annual The manager's ceiling

Five budgets with margins of between 14 % and 17 %. The sum of the three per-account ones (4,950 USD) comes out slightly above the global 4,800, which in this case is acceptable because the global one is planned and acts as a monthly ceiling; if you want the strict pattern, raise the global to 5,200.

rc-anual deserves a separate comment: it is the one that literally translates what the manager asked for. An annual budget accumulates across the whole financial year and warns when the sum of the months elapsed points to exceeding the ceiling. It is the only one that answers the question "are we going to come in on budget for the year?", which no monthly budget can answer.

(2) Thresholds and recipients:

Budget Thresholds Recipients
rc-global 80 % forecasted, 100 % forecasted, 100 % actual Technical lead; the manager only at 100 % actual
rc-produccion 80 % and 100 % forecasted Technical lead + product owner
rc-preproduccion 80 % forecasted, 100 % actual Technical lead
rc-datos 80 % and 100 % forecasted Technical lead + data team
rc-anual 50 %, 75 % and 90 % actual Manager and technical lead

Note that the annual one uses actual and lower thresholds: with a twelve-month horizon, reaching 75 % in September is already an actionable signal.

(3) Automatic action: only in pre-production, and with this policy:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": ["ec2:RunInstances", "rds:CreateDBInstance", "rds:CreateDBCluster",
               "eks:CreateCluster", "elasticache:CreateReplicationGroup",
               "sagemaker:CreateNotebookInstance"],
    "Resource": "*"
  }]
}

Creating expensive resources is denied and nothing existing is touched. In production no action is set at all, for the usual reason: the worst case is an outage during Black Friday, which costs vastly more than the overspend it would avoid. In data none either: a load process interrupted halfway can leave the warehouse inconsistent, which is an incident and not an inconvenience.

(4) November. With a planned budget that assigns November an amount in line with triple the sales — around 8,500 USD globally — and with two accompanying measures: raising the anomaly thresholds during the campaign, because the pattern changes and the detector will generate false positives; and temporarily disabling pre-production's automatic action during Black Friday week, since that is when most testing happens and a freeze at that moment would block the team exactly when it cannot afford it. Both measures are documented with a reversal date, so nobody forgets to switch them back on in December.

Solution to exercise 2

(1) Is it cause for alarm? Not for alarm, but for attention. The data says this: by the 11th, 43.7 % of the budget has been consumed, when the proportional figure would be 35.5 %. The forecast of 1,482 USD exceeds the budget by 5.9 %, which is a small margin and within what a forecast can get wrong. Besides, the alert is of the forecasted type, not actual: there are 20 days left to act. Ignoring it would be a mistake, and so would panicking.

(2) Three checks, in order:

  1. The cost per order. If October has more orders than September and the unit cost is holding or falling, the growth is healthy and the conversation becomes one about the budget amount, not about a technical problem. It is always the first check.
  2. Daily view of the month in Cost Explorer, filtered to the production account. You are looking for a step: if daily spend jumped on one specific day and stayed there, there is a new resource or a configuration change; if the rise is gradual, it is business volume.
  3. Breakdown by service compared with the previous month, looking at the absolute value of the variation. If Fargate, Aurora and the queues are rising, it is real activity. If CloudWatch or data transfer rises, or a service appears that was not there before, it is something else.

(3) Decision and documentation. With orders 22 % higher and the budget projected 5.9 % over, the cost per order is falling noticeably: the system absorbs 22 % more business with 6 % more spend. It is the best possible news.

The right decision is to raise the production budget from 1,400 to 1,600 USD, and to make no emergency optimisations. What gets documented, in the commit that changes the amount in the CDK:

Raise pres-mf-produccion from 1,400 to 1,600 USD

Reason: sustained business growth. October orders are running 22 % above
September and the cost per order is falling from 0.00972 to 0.00891 USD.
The previous budget was set in August on a volume of 180,000 orders per
month; the current volume is ~220,000.
Amount review: January, after the Christmas campaign.
Approved by: management, 2026-10-12.

With two notes that head off future problems: you also need to review the global budget of 2,000 USD, because if production goes up to 1,600 the sum of the specific ones gets too close to the ceiling; and it is worth noting the January review, because October's figure incorporates growth that may not be permanent.

Solution to exercise 3

(1) The policy. Pre-production has a particularity that sets it apart from development: it has to resemble production, and the pipeline deploys to it automatically. That conditions what you can deny:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "CongelarSoloLoCaroYNoAutomatizado",
    "Effect": "Deny",
    "Action": [
      "ec2:RunInstances",
      "rds:CreateDBInstance",
      "rds:CreateDBCluster",
      "eks:CreateCluster",
      "elasticache:CreateReplicationGroup",
      "redshift-serverless:CreateWorkgroup",
      "sagemaker:CreateNotebookInstance"
    ],
    "Resource": "*"
  }]
}

What is included is the creation of databases, clusters, caches and instances, which in pre-production are created by hand for one-off tests and are what drives the bill. What is deliberately left out is ecs:CreateService, ecs:RunTask, elasticloadbalancing:*, lambda:* and cloudformation:*, because all of that is used by the pipeline on every deployment: denying it would break continuous integration, and a team that cannot deploy to pre-production ends up deploying to production without testing, which is infinitely worse than spending 50 USD too much.

(2) Threshold and mode: ACTUAL at 100 % and MANUAL mode.

  • Actual and not forecasted, for the same reason as in development: a wrong forecast must not block an environment the pipeline depends on.
  • Manual and not automatic, and this is the key difference from development. In development the worst case is that Luis cannot bring up a test machine for a few hours: an inconvenience. In pre-production the worst case is that validation of an urgent deployment gets blocked — a security patch, say — and that is already an incident. Manual mode notifies, shows the proposed action and waits for Marta to approve it with a click, which preserves control without automating the risk.

(3) Three things that could go wrong and their mitigation:

Risk Mitigation
The action blocks the pipeline because the policy includes an action the deployment needs Test the policy in development first through a complete deployment cycle; review the CloudTrail events of a real deployment to find out which actions are genuinely invoked
TargetIds set wrongly and the SCP is applied to the whole OU, reaching production Always point at the specific account, never at the OU; deploy the action with the CDK and review the diff in the pull request; test with a policy that only denies a harmless action
Nobody knows how to remove it on a Sunday night Runbook written in mercadofresco-infra/docs/runbooks/, with the exact organizations:DetachPolicy command, who has permission and what gets documented afterwards. And rehearse it once

A fourth risk worth anticipating: the manual action alert may go unapproved if it arrives on a Friday afternoon. The mitigation is not technical but organisational: agreeing that pending manual actions are part of the on-call daily review, which is one of the operational excellence findings 11-01 left open.

Conclusion

MercadoFresco has gone from knowing what it spends to not being able to overshoot without finding out.

You can tell analysing from controlling: Cost Explorer looks backwards and needs someone to remember; Budgets looks forwards and needs nobody to do anything. And you can place them against anomaly detection, which answers a different question — "this does not look like your pattern" — and which does not catch a gradual 3 % monthly rise capable of blowing a budget in six months. Along with the five reasons the 10 USD alarm from 01-02 had become useless, the first of which is the gravest: an alert that is always red does active harm, because it teaches the team to ignore the whole channel.

You have the four budget types — cost, usage, Savings Plans and reservations — with the usage one as the least known and the one that catches what the cost budget cannot see when prices come down. And the periodicities, with the planned budget solving the real problem of the Christmas campaign: different amounts per month, from 2,000 in normal operation to 2,600 in December, so that the month needing most attention is not the very month when every alert gets ignored. With the most valuable side effect: putting a number on December forces you to estimate it, and estimating it forces you to talk to the business.

You have the distinction between thresholds on actual and forecasted cost, and the reason for configuring both: on the 9th with 51 USD spent, the actual threshold does not fire and the forecasted one does, leaving 21 days to correct course. With the two warnings about the forecast — it needs about five weeks of history and it extrapolates any one-off spend — and the three-threshold escalation with the rule that orders it: each threshold must have a different recipient and a different expected action, to the point where MercadoFresco's 50 % deliberately notifies nobody.

You have the ten specific budgets over the 1,749.60 USD bill: global at 2,000, production 1,400, pre-production 290, development 170, tooling 70, governance 55, plus the cross-cutting ones by the analitica tag (220), by the CloudWatch service (170), by NAT usage (900 GB) and the planned Christmas one. With the four design decisions: margins of 10 to 20 %, the sum of the specific ones below the global one so that the ceiling also warns, development as the only one with an action, and budgets that overlap on purpose because they watch different dimensions — and which for that reason are not added together.

You have creation from the console, the CLI and the CDK, which is how they are really maintained, with the three details that trip people up: the user:Key$Value format of the tag filter, which written wrongly creates a budget that filters down to zero and never warns you; IncludeTax and IncludeCredit at false so as to work on the service cost; and UseAmortized enabled from the start, so that the month a commitment is paid does not trip every threshold. Plus notifications over the three channels and the SNS topic policy with aws:SourceAccount and aws:SourceArn, which is the step most often forgotten.

And you have the part with teeth: budget actions, with their three types — IAM policy, SCP and stopping instances — and their two modes, governed by a rule with no convenient exceptions: automatic only where the worst case is an inconvenience; manual where the worst case is an incident. With the complete case of development account 333344445555, which freezes itself at 100 % of actual cost through an SCP that only denies creating expensive resources and touches nothing already running, pointed at the account and never at the OU, with a minimum-permission role and an unblocking runbook. And the real incident that justified all of it: an EKS cluster forgotten over a weekend, 40 USD instead of 220. With the warning to burn into memory: no restrictive automatic action in production, because a policy that prevents scaling on a campaign Friday turns a 300 USD saving into an outage that costs far more.

And you have the routine that sustains it all: the budget reports that open the meeting with a common document, the seven good practices, the four-step procedure for when a budget is exceeded for a legitimate reason — always starting with the unit cost, and always ending with a commit explaining the new amount — and the FinOps framework with its cycle of inform, optimise and operate, its three principles — teams own their spend, decisions are taken on value and not on cost, and the central team enables rather than controls — and the 30-minute monthly meeting that starts with the cost per order, produces a single action and in which nobody gets a surprise in public.

There is one lever left unused, and it is the only one that reduces the bill without changing anything at all in the architecture. Everything MercadoFresco has running — the Fargate tasks that are on 24 hours a day, the Lambda functions invoked every minute, the ElastiCache nodes that have not been switched off in months — is being paid for at on-demand price, that is, at the price paid by someone who might walk away tomorrow. Part of that consumption is completely predictable and will still be there a year from now. And AWS pays you for telling it so in advance.

In 11-05, "AWS Savings Plans", the optimisation cycle closes by committing capacity: the four purchase models with their discount and their risk, how the dollar-per-hour commitment works with a step-by-step numerical example, what each type covers and what it does not — important in an architecture like this one, mostly serverless — how to identify the stable baseline against the elastic part, and MercadoFresco's specific purchase plan with its monthly and annual saving. With the ordering that 11-03 already flagged and that is fully justified here: first switch off what is spare, then commit.

© Copyright 2026. All rights reserved