The FinOps Round: Where the Money Goes
Reading a bill by line item, the commitment ladder priced out, the four wastes that account for most overspend, and unit cost as the metric that survives growth.
“This bill is $180,000 a month, what do you do?” The wrong move is to start proposing savings. The right move is to ask for the breakdown.
Read the bill first
aws ce get-cost-and-usage --time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--query 'ResultsByTime[0].Groups[].{svc:Keys[0],cost:Metrics.UnblendedCost.Amount}' \
--output text | sort -k1 -rn | head -10
61204.55 Amazon Elastic Compute Cloud - Compute
38911.20 Amazon Relational Database Service
27340.88 EC2 - Other
19882.71 Amazon Simple Storage Service
12405.33 Amazon Elastic Container Service for Kubernetes
8776.02 AmazonCloudWatch
5120.44 Amazon Data Transfer
3902.17 AWS Lambda
2611.09 Amazon Simple Queue Service
1015.60 AWS Key Management Service
EC2 - Other at $27,340 is the line to ask about, because nobody budgets for it. It is where
NAT gateways, EBS volumes, elastic IPs, and load balancer capacity units land — and it is
routinely the third-largest line and the least understood.
aws ce get-cost-and-usage --time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY --metrics UnblendedCost \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["EC2 - Other"]}}' \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--query 'ResultsByTime[0].Groups[].{u:Keys[0],c:Metrics.UnblendedCost.Amount}' --output text \
| sort -k1 -rn | head -5
11847.30 NatGateway-Bytes
7220.15 EBS:VolumeUsage.gp3
4881.02 NatGateway-Hours
2109.44 EBS:SnapshotUsage
1282.97 LoadBalancerUsage
$11,847 of NAT gateway data processing. From the storage lesson, a gateway VPC endpoint removes object-storage traffic from that line for free — a single API call against the largest sub-line in the third-largest service.
That is the shape of a good cost answer: find the line, explain the mechanism, name the fix and its cost.
The commitment ladder
# commitments.py
ON_DEMAND_HOURLY = 0.0832
HOURS = 730
BASE = ON_DEMAND_HOURLY * HOURS
OPTIONS = {
# discount commitment flexibility
"on demand": (0.00, "none", "total"),
"savings plan, 1yr, no up": (0.28, "1 year", "any instance/region"),
"savings plan, 3yr, no up": (0.46, "3 years", "any instance/region"),
"reserved, 1yr, no upfront": (0.34, "1 year", "family + region"),
"reserved, 3yr, all upfront": (0.60, "3 years", "family + region"),
"spot": (0.66, "none", "can be reclaimed"),
}
INSTANCES = 100
print(f"{'option':<28} {'$/mo each':>10} {'100 instances':>15} {'saved/yr':>12}")
for name, (disc, commit, flex) in OPTIONS.items():
each = BASE * (1 - disc)
fleet = each * INSTANCES
saved = (BASE - each) * INSTANCES * 12
print(f"{name:<28} {each:>10.2f} {fleet:>15,.0f} {saved:>12,.0f}")
$ python commitments.py
option $/mo each 100 instances saved/yr
on demand 60.74 6,074 0
savings plan, 1yr, no up 43.73 4,373 20,407
savings plan, 3yr, no up 32.80 3,280 33,526
reserved, 1yr, no upfront 40.09 4,009 24,780
reserved, 3yr, all upfront 24.29 2,429 43,730
spot 20.65 2,065 48,103
A hundred instances on three-year reserved saves $43,730 a year against on demand. Spot saves more and can be reclaimed with two minutes’ notice.
The tradeoff to state clearly:
“A three-year commitment saves 60% and locks in an instance family for three years. If there is any chance of moving to containers or serverless in that window, a savings plan is the right instrument — slightly less discount, but it applies across instance types and even across compute services. I’d commit to the trough of the usage graph, not the average, and cover the rest with on demand or spot.”
Commit to the trough, not the average is the specific piece of advice worth having. Committing to the average guarantees you pay for unused commitment during quiet periods.
The four wastes
# 1. unattached volumes
aws ec2 describe-volumes --filters Name=status,Values=available \
--query 'Volumes[].{id:VolumeId,gb:Size,type:VolumeType,since:CreateTime}' --output table
--------------------------------------------------------------------
| DescribeVolumes |
+------+--------------------------+----------+---------------------+
| gb | since | type | id |
+------+--------------------------+----------+---------------------+
| 500 | 2024-03-11T09:14:22Z | gp3 | vol-0a1b2c3d |
| 200 | 2025-01-08T17:40:03Z | gp3 | vol-0e4f5a6b |
| 1000| 2023-11-22T11:02:55Z | io2 | vol-0c7d8e9f |
+------+--------------------------+----------+---------------------+
Three volumes attached to nothing, one since 2023. A 1 TB io2 volume is roughly $125 a month for storage plus provisioned IOPS — paid every month for two years for nothing.
# 2. idle load balancers
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerArn' --output text | \
xargs -n1 -I{} aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB \
--metric-name RequestCount --statistics Sum --period 604800 \
--start-time 2026-09-03T00:00:00Z --end-time 2026-09-10T00:00:00Z \
--dimensions Name=LoadBalancer,Value={} --query 'Datapoints[0].Sum'
None
None
14882301.0
Two of three load balancers served zero requests in a week, at roughly $16 a month each in base charges before capacity units.
# 3. non-production running out of hours
aws ec2 describe-instances --filters Name=tag:Environment,Values=dev,staging \
Name=instance-state-name,Values=running \
--query 'Reservations[].Instances[].InstanceType' --output text | sort | uniq -c
14 m7g.large
6 m7g.xlarge
2 r7g.2xlarge
DEV_MONTHLY = 14*60.74 + 6*121.48 + 2*485.94
BUSINESS_HOURS = 10 * 22 # 10 hrs x 22 weekdays
print(f"running 24/7: ${DEV_MONTHLY:>9,.2f}")
print(f"business hours only: ${DEV_MONTHLY * BUSINESS_HOURS / 730:>9,.2f}")
print(f"saved: ${DEV_MONTHLY * (1 - BUSINESS_HOURS/730):>9,.2f}/month "
f"({100*(1-BUSINESS_HOURS/730):.0f}%)")
running 24/7: $ 2,550.91
business hours only: $ 768.79
saved: $ 1,782.12/month (70%)
70% off every non-production environment, from a scheduled stop-start and nothing else. It is the single highest ratio of saving to effort on the list.
# 4. oversized instances
aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-0abc --statistics Average,Maximum \
--start-time 2026-08-10T00:00:00Z --end-time 2026-09-10T00:00:00Z --period 2592000 \
--query 'Datapoints[0].{avg:Average,max:Maximum}'
{
"avg": 4.2,
"max": 18.7
}
4.2% average, 18.7% peak over thirty days. That instance is four sizes too large, and the fix is one restart.
The caveat worth adding, because it is what separates a careful answer:
“CPU alone is not enough to rightsize. Memory is not reported by default on most platforms, so an instance at 4% CPU might be at 90% memory. I’d want memory and network metrics from the agent before resizing, and I’d rightsize in one step rather than several so there is one change to attribute a regression to.”
Unit cost is the metric that survives growth
# unitcost.py
MONTHS = [
# month cost orders
("2026-04", 98_400, 1_200_000),
("2026-05", 112_800, 1_580_000),
("2026-06", 131_200, 2_010_000),
("2026-07", 148_900, 2_640_000),
("2026-08", 180_300, 2_710_000),
]
print(f"{'month':<9} {'cost':>10} {'orders':>12} {'cost/1k orders':>16} {'change':>9}")
prev = None
for month, cost, orders in MONTHS:
unit = cost / orders * 1000
delta = f"{100*(unit-prev)/prev:+.1f}%" if prev else "—"
print(f"{month:<9} {cost:>10,} {orders:>12,} {unit:>16.2f} {delta:>9}")
prev = unit
$ python unitcost.py
month cost orders cost/1k orders change
2026-04 98,400 1,200,000 82.00 —
2026-05 112,800 1,580,000 71.39 -12.9%
2026-06 131,200 2,010,000 65.27 -8.6%
2026-07 148,900 2,640,000 56.40 -13.6%
2026-08 180,300 2,710,000 66.53 +18.0%
The total bill rose every month. The unit cost fell for three months and then jumped 18% in August — and that jump, not the total, is the thing to investigate. Orders grew 2.7% while cost grew 21%.
“A rising bill is not automatically a problem — it is a problem when unit cost rises. I’d track cost per order or per active user, and treat a rising unit cost as the alert. It also makes the conversation with finance a different one: ‘we spent 21% more and served 2.7% more orders’ is a real finding, where ‘the bill went up’ is not.”
That reframing is what a FinOps round is testing for.
Tagging, and the untagged problem
aws ce get-cost-and-usage --time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=TAG,Key=Team \
--query 'ResultsByTime[0].Groups[].{tag:Keys[0],cost:Metrics.UnblendedCost.Amount}' --output text
Team$ 41203.88
Team$data 52910.44
Team$api 38774.10
Team$web 27661.30
Team$ml 19620.55
Team$ with no value is untagged: $41,203, or 23% of the bill, belongs to nobody. You cannot
allocate it, nobody will optimise it, and it is where orphaned resources accumulate.
The enforcement worth naming:
tag policies define which tags are required and their allowed values
SCP denying RunInstances without the required tags — hard enforcement
IaC-only provisioning tags applied by default in the module, not by hand
cost anomaly detection alarms on an unusual spend pattern, per service
Retroactive tagging never finishes. Enforcing at creation is the answer, and saying that directly is better than describing a cleanup project.
Recognising it
QUESTION ANSWER
"the bill is too high" get the breakdown first; do not guess
"what's the biggest saving?" non-prod schedules (70%), then commitments
"reserved or savings plan?" savings plan unless the shape is fixed
"how much can you save?" 20-40% without rearchitecting; name the ceiling
"EC2-Other is huge" NAT gateway data, orphaned EBS, idle LBs
"our bill grows with users, is that ok?" depends on unit cost, not total
"who owns this spend?" tags — and enforce them at creation
"data transfer is 30% of our bill" VPC endpoints, CDN, keep traffic in-AZ
"we're on Kubernetes and it's expensive" node utilisation; bin-packing before scaling
Practice
1. Break down EC2 - Other by usage type.
NatGateway-Bytes $11,847 EBS gp3 $7,220 NatGateway-Hours $4,881
The third-largest service line and the least understood. A gateway VPC endpoint removes the largest sub-line for free.
2. Price 100 instances across the commitment ladder.
on demand $6,074/mo 3yr reserved $2,429/mo $43,730 saved per year
Commit to the trough of the usage graph, not the average — committing to the average guarantees unused commitment in quiet periods.
3. Schedule non-production to business hours.
$2,551/mo → $769/mo, a 70% saving from a stop-start schedule
The highest ratio of saving to effort available. It needs no architectural change.
4. Track cost per thousand orders instead of total cost.
Unit cost fell three months, then rose 18.0% in August.
Cost +21%, orders +2.7% — that is the finding.
A rising bill is not automatically a problem. A rising unit cost is.
Next: the architecture design round, end to end.