Skip to main content
Cloud Interviews intermediate Lesson 8 of 10

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.

Frequently Asked Questions

What is the first thing to do about a large cloud bill?
Get the breakdown by service and by tag before changing anything. Optimisation without measurement usually targets whatever is most familiar rather than what is most expensive, and the largest line is regularly something nobody was watching — data transfer, idle non-production environments, or orphaned storage.
Reserved instances or savings plans?
Savings plans for compute you will keep using but might move between instance types or services; reserved instances when the shape is genuinely fixed and you want the deepest discount. Both trade flexibility for price, and both are wasted if you commit to capacity you then rearchitect away.
What is unit cost and why does it matter?
Cost divided by a business unit — per user, per order, per gigabyte processed. A total bill that grows tells you nothing on its own; unit cost tells you whether growth is efficient. It is also the number that lets you say a bill going up is fine, which is the mature version of a cost answer.
How much can be saved without changing architecture?
Usually 20-40%, from commitments, rightsizing, non-production schedules, and deleting orphaned resources. Beyond that requires architectural change — caching, tiering, or moving off a service. Naming that ceiling is a stronger answer than promising an unbounded saving.