AWS Spend Guardrail Line Check
If spend runs away tonight, what actually stops it? A read-only inventory of every AWS Budget, every attached action, and what each one is really allowed to deny.
Standardized doc shell (hero + metadata + quick path). Custom content below remains page-specific.
Download Tool FilesStandard run path
Structured quick-reference sections for prerequisites, installation, usage, and troubleshooting.
Prerequisites
- Python 3.9 or newer
- boto3 and botocore, installed with pip install boto3
- Credentials through the normal boto3 chain: a named profile, environment variables, or an instance or container role
- No local write access required unless you ask for --output-csv or --output-json
Permissions, and what you lose without each one
The script degrades instead of failing. Every optional permission you withhold turns a measured answer into an honestly labelled unknown, never into a false pass.
| Permission | Needed for | Without it |
|---|---|---|
| budgets:DescribeBudgets | The inventory itself | Nothing runs |
| budgets:DescribeNotificationsForBudget | Naming alert-only budgets | Alert counts read as zero |
| budgets:DescribeBudgetActionsForAccount and ...ForBudget | Finding attached actions | Every budget looks alert-only |
| sts:GetCallerIdentity | Resolving the account id | Nothing runs |
| iam:GetPolicy and iam:GetPolicyVersion | Reading what an IAM action denies | The row reads "action attached, contents not evaluated" |
| organizations:DescribePolicy | Reading what an SCP denies | Same honest fallback, and this one only resolves from the management account |
| budgets:DescribeBudgetActionHistories | Whether an action has ever fired | History reads unreadable, never zero |
| ce:GetCostAndUsage | The gap list from your own bill | The gap list is absent, and nothing generic is substituted for it |
| organizations:ListAccounts and sts:AssumeRole | Organization mode | The run covers one account and says so |
Every call is a describe, get or list. There is no write path in this script, so there is no write path to disable.
Standard run path
- 1Run it against one account first and read the VERDICT line under each budget. That is the whole answer in one sentence per budget.
- 2Check the approval model on every action. AUTOMATIC fires on its own; MANUAL waits for a human, which at 02:00 means it waits.
- 3Look for any row reading "action attached, contents not evaluated" and fix the permission or read that policy by hand. An unread control is not a control.
- 4Add --leak-list once you want the gap list derived from your own bill rather than a generic one. It costs one cent per Cost Explorer request.
- 5Add --org only when you have confirmed the role name and its trust policy, because budgets are per account and a single-account run is a single-account answer.
python aws_spend_guardrail_line_check.py --profile myprofilepython aws_spend_guardrail_line_check.py --profile myprofile --leak-listpython aws_spend_guardrail_line_check.py --profile management \
--org --org-role OrganizationAccountAccessRolepython aws_spend_guardrail_line_check.py --profile myprofile \
--output-csv guardrails.csv --output-json guardrails.jsonReading the verdict
One line per budget, and only four things it can say
The verdict is the sentence you paste into the ticket. Everything above it is the evidence.
| Verdict | What it means |
|---|---|
| no notifications and no actions, this budget is decoration | It exists in the console and does nothing at all |
| alerting only, nothing stops anything | Somebody will get an email. Nothing will stop |
| action attached but every one is MANUAL | The control is real and it waits for a human to approve it before anything happens |
| N automatic actions attached: denies ... | Something fires on its own, and this is the exact list of action strings it denies |
A fifth outcome exists and it is deliberate: when an automatic action is attached but its policy could not be read, the verdict says coverage is unknown rather than guessing either way.
- AWS Budgets data refreshes up to three times a day, typically 8 to 12 hours apart, so the spend figures in the output are not live.
What this tool does not tell you
- It does not simulate policy evaluation. It reports the Deny statements it finds, and whether a specific API call is denied at runtime still depends on the full policy chain including permission boundaries, session policies and other SCPs.
- It does not price anything and claims no savings. It measures control coverage.
- It cannot tell you a Deny stops billing. A deny on resource creation does not stop metered consumption by resources that already exist.
- It does not evaluate a policy it could not read. That case is labelled, not inferred.
The most common false pass
A budget with an action attached looks protected in a screenshot. If that action is MANUAL, or if its policy denies three action strings that have nothing to do with your top billable line, the screenshot is the only protection you have. That gap is the reason this script reads the policy instead of trusting the attachment.
Troubleshooting
- AWS Budgets uses a single global endpoint, so the script pins its Budgets client to us-east-1. If you have patched that pin out, it will fail anywhere no default region is configured.
- Rows reading "contents not evaluated" are a permissions result, not a bug. Grant iam:GetPolicy and iam:GetPolicyVersion, and run SCP resolution from the management account.
- If the gap list is absent, check ce:GetCostAndUsage and that Cost Explorer is enabled for the account. The script reports the reason rather than substituting a generic list.
- In organization mode, accounts you cannot assume into are listed as errors at the end of the report and the exit code is 2. They are never silently skipped.
Why this exists
Search for whether AWS will stop spending at a number you choose and you get forum threads. The honest answer needs three qualifiers, and once you add them the answer changes: there is no account-wide, customer-chosen, dollar-denominated cap that AWS will enforce for you. What AWS does sell is a budget that can trigger an action, and the distance between those two things is where estates get surprised.
This script closes that distance for one account at a time. It does not tell you what AWS offers in general, which is what the hard-cap inventory post is for. It tells you what your account has wired right now, with the policy documents read rather than assumed.
What it measures
Every budget, and every notification on it
Type, limit, period, actual and forecasted spend, and the notification count. A budget with notifications and no actions is named alert-only rather than left looking protective.
Automatic versus manual, per action
AWS exposes an approval model on each budget action. AUTOMATIC fires on its own. MANUAL waits for a human. This is the single field that separates a control from a notification, and it is the first thing the report prints.
What the attached policy actually denies
For an IAM action it fetches the default policy version and parses the document. For a service control policy it fetches the content from Organizations. Both come back as a list of real denied action strings, not a guess based on the attachment existing.
Whether the action has ever fired
The action history answers a question a console screenshot cannot: has this thing ever actually executed? If the history cannot be read, the answer is unknown, never no.
Your gap list, from your own bill
With --leak-list it pulls your top billable usage types from Cost Explorer and classifies each one against the denies your own automatic actions carry. A generic list of usual suspects would be identical for every account, which is why this one is not generic.
Read-only by construction
Every call is a describe, get or list. There is no flag to disable, no dry-run mode to remember and no mutating path to review, because none exists in the source.
The gap list, and why it has to come from your bill
Every write-up of this problem ends with the same three examples: data transfer, S3 requests, Lambda invocations. They are fine examples and they are also the same three for everybody, which makes them a reading recommendation rather than a finding about your account.
So the gap list here is built the other way round. It takes your top billable usage types this month, takes the union of denied action strings from the actions that actually fire without a human, and classifies each billed line against them. A line comes back in one of five states: no creation action exists to deny it at all, no automatic deny exists in the account, a broad deny matches, a creation-scoped deny matches, or the service is not in the script's prefix table and needs a human read. That last state is included on purpose. A tool that quietly maps an unrecognised service to "not covered" is inventing a finding.
Even a broad deny that matches your biggest line does not remove the charge. It stops new resources. Everything already running keeps billing at the same rate, which is why the classifier names the scope instead of printing a pass.
What the run costs
AWS Budgets read operations are not documented as chargeable. Neither the AWS Budgets pricing page nor the AWS Cost Management pricing overview states a per-request charge for the Budgets describe operations, so this page claims no charge is documented rather than claiming the run is free.
Cost Explorer is different and it is documented: each ce:GetCostAndUsage request is billed at 0.01 USD. That is why the gap list sits behind an explicit --leak-list flag, why the report prints how many Cost Explorer requests it made and what they cost, and why --max-ce-pages exists to bound it. A single-page run costs one cent.
How it was tested
Verified on 5 August 2026 without any live AWS account, and the specifics matter more than the word verified. All four Budgets operations it calls were confirmed to exist in the botocore service model and to have paginators, and the script routes all four through a paginator. The Budgets client was confirmed to resolve to the single global endpoint when pinned to us-east-1, which is why the pin is not optional. A 62-check offline suite drove the real code paths against canned API responses using botocore Stubber, which validates fixtures against the live service model, covering policy parsing, the automatic-versus-manual verdicts, AccessDenied degradation, the gap-list classifier, CSV output and the failure exits. An AST walk over the source confirms no mutating AWS operation is reachable.
Not verified: nothing was run against a real AWS account. Live response shapes at scale, cross-account assume-role behaviour and Cost Explorer service-name coverage in a real bill are all untested. Treat the first run as the read-only dry run it is and check the output against the console. The full disclosure, the checksum and the minimum access list are recorded in the tool's trust metadata.
Read the inventory this came from
The script answers the question for your account. The post answers it for the three clouds, using each vendor's own limitation text, including the handful of real enforced caps that do exist and why none of them is the account-wide cap people assume they are buying.
What Actually Stops Cloud SpendWhat to do next
Pick the path that fits where you are right now.