💸 Cloud Cost Control: Stop Surprises Before They Start
Why a cloud bill surprises people, how late every cost alert really is (with the numbers from each provider's documentation), what an alert can and cannot stop, the hour arithmetic behind forgotten resources, and a routine for before, during and after an experiment
💡 How this page was made. The arithmetic was run for real as small Python programs on 6 October 2026: hours in a month, the cost of a forgotten public IP address, and how much money accrues during each alert delay. The delays and limits are from the providers' documentation, read on 6 October 2026 (AWS Budgets and Cost Anomaly Detection, Azure Cost Management, Google Cloud budget notifications, Oracle Cost Analysis and budgets). The hourly rate used in the delay table (3 per hour) is a made-up example, not a real price, and the only real price on the page is AWS's public IPv4 charge of $0.005 per hour, which AWS publishes. Nothing was run on a cloud account. The traps here are collected from the earlier pages of this series: the services maps (Azure, AWS, Google Cloud, Oracle), the networking, containers, Kubernetes and EC2 pages, and the free VM guides.
Why bills surprise people
Almost every surprising cloud bill comes from one of three things:
- Something still exists. A server that was stopped but not deleted, a disk, a snapshot, a public IP address, a load balancer, a NAT gateway, a database, a Kubernetes cluster. Most cloud resources bill for existing, not for being used.
- Data moved. Traffic out of the cloud, and between regions, is billed. A test that copies a lot of data somewhere can cost more than the servers.
- Something scaled by itself. Autoscaling, a loop that creates resources, a job that never stops, an application that logs without limit.
And there is a fourth thing that makes the other three worse: you find out late. The next section has the numbers.
Every alert is late
Cost data is not live. It arrives in batches, and the alerts run on the batches. From the providers' documentation (read on 6 October 2026):
- AWS Budgets information is updated up to three times a day, typically 8 to 12 hours apart. AWS adds that there can be a delay between a charge and the notification, so you can pass the threshold before the alert arrives.
- AWS Cost Anomaly Detection uses machine learning to spot unusual spend. It runs about three times a day on data from Cost Explorer, which has a delay of up to 24 hours, so it can take up to 24 hours to detect an anomaly after the usage. A new monitor needs 24 hours before it detects anything, and a new service needs 10 days of history before anomalies in it can be detected.
- Azure budgets: cost and usage data is typically available 8 to 24 hours late, and budgets are evaluated every 24 hours. A brand-new subscription may need up to 48 hours before you can use all Cost Management features. Azure's anomaly detection compares each day's usage with a forecast based on the last 60 days, and it runs 36 hours after the end of the day (UTC). An anomaly alert email is sent once, there is a limit of five anomaly alert rules per subscription, and they are available in the public cloud only.
- Google Cloud budget notifications go to a Pub/Sub topic "multiple times per day", and it may take several hours before you receive the first one. Messages might arrive out of order.
- Oracle Cloud: budget alert rules are evaluated every 24 hours, and data can take up to 48 hours to appear in Cost Analysis.
Conclusion: there is a blind spot of half a day to two days on every cloud. What can accrue in it depends on the rate. An illustration with a made-up rate of 3 per hour (a larger machine, a gateway, a database; not a real price), using the delays above (as run):
>>> delays = {'AWS Budgets (updates up to 3 times a day)': 12, 'Azure cost data (8 to 24 hours)': 24, 'Cost Anomaly Detection (up to 24 hours)': 24, 'Oracle Cost Analysis (up to 48 hours)': 48}
>>> example_rate = 3.00
>>> for name, hours in delays.items():
... print('%-46s %2d h -> %6.2f accrues unseen at %.2f/hour' % (name, hours, hours * example_rate, example_rate))
...
AWS Budgets (updates up to 3 times a day) 12 h -> 36.00 accrues unseen at 3.00/hour
Azure cost data (8 to 24 hours) 24 h -> 72.00 accrues unseen at 3.00/hour
Cost Anomaly Detection (up to 24 hours) 24 h -> 72.00 accrues unseen at 3.00/hour
Oracle Cost Analysis (up to 48 hours) 48 h -> 144.00 accrues unseen at 3.00/hour
>>> print('a resource that runs for a weekend (60 hours):', 60 * example_rate)
a resource that runs for a weekend (60 hours): 180.0
>>> print('and a month (730 hours):', 730 * example_rate)
and a month (730 hours): 2190.0
- During a delay of 12 to 48 hours, 36 to 144 units accrue before any alert could show it.
- A resource forgotten over a weekend costs 180 at that rate, and over a month 2,190.
- This is why small, fast habits (look at the cost page yourself during an experiment, delete at the end of the day) beat any alert.
An alert is not a limit
Every budget on every cloud warns. From the documentation: AWS Budgets notifies you and can optionally act (below); Azure says "Resources aren't affected, and your consumption isn't stopped"; Google says an alerts-only budget "doesn't automatically cap" usage; Oracle calls budgets "soft limits" that "do not enforce spending limits". What each cloud lets you add on top:
- AWS Budgets actions: when a threshold is reached, AWS can apply an IAM policy (for example one that denies creating more EC2 resources), apply a service control policy, or target specific EC2 or RDS instances, either automatically or after your approval. It needs an IAM role for Budgets, and you cannot target instances in another account.
- Google Cloud: a budget can publish to Pub/Sub, and the documentation shows a function that disables billing on the project when a threshold is passed. That halts every service in the project immediately and can cause data loss, and because of the delays "overspending may occur before automated responses trigger". It is a last-resort fuse for a throwaway project, not for anything you care about.
- Azure: budgets can call an action group (email, push, automation). Anomaly alerts arrive by email, which you can feed into Logic Apps or ticket systems.
- Oracle Cloud: alert rules send email. I did not read of an automatic enforcement action.
What actually caps spending: the free account types, by their design: the AWS free plan (no charges until you upgrade, and the account closes when it ends), the Azure free account (the subscription is disabled when the credit ends), the Google Cloud trial (resources stop when it ends) and the Oracle trial (paid resources are reclaimed). The account pages (Azure, AWS, Google Cloud, Oracle) have the details. On a normal paid account, in the documentation I read, there is no built-in hard cap.
The arithmetic of hours
Most cloud compute bills by the hour (or the second), so the question is how many hours. As run:
>>> import calendar >>> [calendar.monthrange(2026, m)[1] * 24 for m in (2, 4, 10)] [672, 720, 744] >>> round(365 * 24 / 12, 1) 730.0 >>> one_vm = 31 * 24 >>> one_vm 744 >>> 2 * one_vm 1488 >>> free_hours = 750 >>> free_hours - one_vm 6 >>> round(free_hours / 24, 1) 31.2 >>> office_hours = 10 * 5 * 52 / 12 >>> round(office_hours) 217 >>> round(100 * office_hours / (365 * 24 / 12)) 30
- A month has 672 to 744 hours (February with 28 days, April with 30, October with 31), 730 on average.
- One machine running all month: 744 hours in October. A free offer of 750 hours a month (as on the older AWS free tier and Azure's free VM sizes: the free VM guides say so) covers exactly one machine with 6 hours to spare. A second machine runs out the hours in about half a month: 1,488 against 750.
- 750 hours is 31.2 days: it fits a 31-day month, and not two machines.
- A machine used only in office hours (10 hours, 5 days a week) runs about 217 hours a month, 30 percent of the always-on figure. Stopping machines at night and on weekends is the biggest saving available for development and test servers. (On AWS and Azure, stop from the console, CLI or API so that compute billing stops: Azure's "stopped (deallocated)" state, AWS's stopped state that keeps the disks billing.)
A forgotten public IPv4 address on AWS is billed at $0.005 per hour (AWS's announcement, effective February 1, 2024), whether it is in use or idle:
>>> ipv4_per_hour = 0.005
>>> for hours in (730, 744):
... print(hours, 'hours ->', round(ipv4_per_hour * hours, 2), 'per address per month')
...
730 hours -> 3.65 per address per month
744 hours -> 3.72 per address per month
>>> print('five forgotten addresses:', round(5 * ipv4_per_hour * 730, 2), 'per month')
five forgotten addresses: 18.25 per month
>>> print('one forgotten address for a year:', round(ipv4_per_hour * 24 * 365, 2))
one forgotten address for a year: 43.8
That is 3.65 to 3.72 a month per address, 43.80 for a year for one address nobody remembers, and 18.25 a month for five. Small, but nothing stops it: it is the typical "I only deleted the machine" leftover.
The usual suspects, in one list
Collected from this series. Things that bill while they exist, even if idle:
- Virtual machines that are stopped but not removed. Their disks keep billing on every cloud (AWS EBS volumes, Azure managed disks, Google persistent disks, Oracle boot and block volumes), as do static IP addresses. On Azure, a VM must be deallocated, not just shut down inside the operating system. On Google Cloud a stopped VM stops costing for compute but not for its disks and static IPs. On Oracle, how a stopped instance bills is shape-dependent: terminate what you do not need.
- Snapshots and images. Incremental, but billed for the data stored, and deleting one may not reduce the cost if other snapshots use the same blocks (AWS).
- Public IP addresses and Elastic IPs. By the hour.
- NAT gateways, load balancers, VPN gateways, Bastion and similar network appliances: by the hour plus data, used or not. Kubernetes Services of type LoadBalancer create real load balancers.
- Managed databases: billed while they exist, idle or not (serverless tiers are the exception).
- Kubernetes clusters: nodes bill (on every cloud), and EKS charges per cluster besides, while AKS's control plane is free. Delete Services and volumes before the cluster, so nothing is orphaned.
- Container services: Oracle Container Instances bill while Active or Updating, not while stopped, and a stopped one still counts against service limits.
- Container registries bill for stored images and transfer: delete old images (ECR has lifecycle policies for it).
- Logs and monitoring data: stored and ingested volumes add up quietly.
Data that moves: traffic leaving the cloud, traffic between regions, and in some cases between zones (check each provider's networking pricing page: the networking page says what is documented).
Finding who and what: attribution
When the number is wrong, you need to answer "which resource, and whose?" Set that up before you need it:
- One account, resource group, project or compartment per experiment, so that "delete the lot" is a single action (the Azure resource group, the Google Cloud project, the Oracle compartment; AWS has no group, so use a separate account or tags and a CloudFormation or Terraform stack).
- Tag everything with at least an owner, a project and an expiry date. Notes from the documentation: on Azure, tags put on a resource group are not inherited by its resources; on Oracle, tag-based costs follow the date the tag was associated and are not retroactive, so tag at creation.
- Use the cost analysis tool the way the documentation does: group by service, then by resource (Oracle: group by "Resource OCID" and filter by service), then by meter or SKU. Azure's own example walks through finding a cost jump: group by resource group, then by resource, then by meter, and it finds that a VPN gateway changed from a small SKU to a much larger one on one day. Allow for the delay: a change in the middle of a day only shows its full effect the next full day.
- Ask who owns it: Azure suggests the activity log (who made the last change), the people with write access to the parent scope, and tags.
A routine
Before you create anything
- A budget with an email you read, and an anomaly alert if the cloud has one (AWS Cost Anomaly Detection, Azure anomaly alerts). Remember the delays.
- A calendar entry for the delete date, and a tag that says it.
- Infrastructure as code so that the clean-up is one command (Terraform: terraform destroy).
- A separate account, subscription, project or compartment for the experiment.
- Free-tier limits written down (the hours, the sizes, the regions). A free tier is a list of exceptions: anything outside it bills.
While it runs
- Look at the cost page yourself at the end of each day, not only the alert. Look for new line items.
- Stop what you are not using, correctly (deallocate on Azure).
When you finish
- Delete the whole container (resource group, project, compartment, stack) rather than resources one by one.
- List what remains in every region: disks, snapshots, IPs, load balancers, buckets, registries. (AWS: EC2 Global View.)
- Check the bill the next day and again three days later, because of the delays above.
- Remove the budget and the alert rules only after that, not before.
When the number is already wrong
- Stop the meter first: stop or delete the biggest resources (find them in the cost analysis tool, grouped by resource), even before you understand why.
- Find the cause: new resource, changed size, more usage, more data out? The three types of change in Azure's guide are new costs, removed costs and changed costs.
- Check the access: a surprise resource you did not create may mean leaked credentials (see the IAM page): rotate keys, remove unknown identities, and review the audit logs.
- Contact the provider's support with the dates and the resource IDs. I did not read any provider's policy on refunds for mistakes, so this page makes no promise about them.
Common mistakes
- Treating a budget as a limit.
- Trusting the alert instead of looking.
- Stopping a VM and thinking it is free.
- Deleting the VM and leaving the disk, the IP and the snapshot.
- Forgetting another region.
- Leaving a Kubernetes cluster or a database running overnight "to continue tomorrow".
- No tags, so nobody knows what to delete.
- Using a work account for a personal test, so the bill is somebody else's problem (the account pages explain).
- A hard-stop automation on something that matters: "disable billing" on a project that holds real data.
What this page did not cover
Reserved capacity and savings plans, spot and preemptible machines, right-sizing, showback and chargeback in companies, billing exports and dashboards, and provider pricing in detail: read each provider's pricing pages and calculators, since prices change and differ by region.
The cheat sheet
- Three causes: it still exists, data moved, it scaled. Fourth: you found out late.
- Delays: AWS Budgets up to 3 updates a day (8 to 12 hours apart), AWS anomaly detection up to 24 hours (new service: 10 days of history). Azure data 8 to 24 hours, budgets checked every 24 hours, anomaly detection 36 hours after the day ends, 5 anomaly rules per subscription. Google Pub/Sub several times a day, first one after hours. Oracle budgets every 24 hours, Cost Analysis up to 48 hours.
- Alert, not limit: AWS Budgets actions (IAM or SCP deny, target EC2 and RDS), Google Pub/Sub then a function that disables billing (dangerous), Azure action groups, Oracle email. Free plans and trials are the only real caps.
- Hours: 730 a month on average (672 to 744). One VM 24/7 is 744. Office hours only is about 30 percent. A 750-hour free allowance fits one machine.
- Public IPv4 on AWS: $0.005 per hour, about 3.65 to 3.72 a month each.
- Suspects: stopped-but-not-deleted machines (disks, static IPs), snapshots, IPs, NAT, load balancers, VPN gateways, databases, Kubernetes nodes and control planes, registries, logs, data out.
- Routine: budget and anomaly alert, tag with owner and expiry, a separate container per experiment, IaC, look daily, delete the whole container, list leftovers in every region, check the bill after 1 and 3 days.
← EC2 From Scratch: A Server You Can Trust · ↑ All tutorials · First Boot on Every Cloud: cloud-init and User Data →