FinOps
The first real invoice of BoxOffice's production environment arrives, and it is three times the estimate. Nobody can say why: the costs are spread over forty line items, half of them have names like USE1-NatGateway-Bytes and DataTransfer-Regional-Bytes, and no resource is tagged with the service that created it. In the cloud, every architectural decision is also a purchasing decision β and the bill is the one metric that every executive reads.
FinOps (cloud financial operations) is the practice of bringing financial accountability to the variable spending model of the cloud, so that engineering, finance and business make trade-offs between speed, cost and quality together1. For a system designer it comes down to three skills: estimate what an architecture will cost, attribute what it actually costs, and optimize without breaking the SLOs2.
The FinOps lifecycle
The FinOps Foundation organizes the practice in three phases that repeat continuously1:
flowchart LR
inform["Inform<br/>visibility: tags, allocation,<br/>showback, unit costs"] --> optimize["Optimize<br/>rightsizing, commitments,<br/>spot, architecture changes"]
optimize --> operate["Operate<br/>budgets, anomaly alerts,<br/>policies, reviews"]
operate --> inform - Inform β you cannot optimize what you cannot see. Every resource carries tags (
project,env,service,team); Kubernetes costs are split by namespace and label using the pods' resource requests (tools such as the CNCF project OpenCost4, or Kubecost). The result is showback (teams see their costs) or chargeback (teams are billed internally). - Optimize β reduce waste (idle and oversized resources) and pay less per unit (commitments, spot, cheaper architectures).
- Operate β budgets with alerts, anomaly detection on daily spend, policies (no untagged resources, no GPU instances in dev), and a monthly cost review next to the SLO review.
Anatomy of a cloud bill
| Cost type | Billed by | BoxOffice examples | Watch out for |
|---|---|---|---|
| Compute | vCPU-hour and GB-hour (VMs, containers), or request + GB-second (FaaS) | Kubernetes nodes, Lambda thumbnails | Idle replicas, oversized requests |
| Managed services | Instance-hour + storage + I/O | PostgreSQL, Redis, Kafka | Multi-AZ doubles the instance; replicas add up |
| Storage | GB-month, plus requests | S3 ticket PDFs, database volumes, snapshots | Snapshots and logs kept forever |
| Data transfer | GB moved | CDN egress to fans, cross-AZ traffic between services, NAT processing | Internet egress and cross-AZ traffic are charged; ingress usually is not |
| Fixed hourly items | Hour, whether used or not | Kubernetes control plane, load balancers, NAT gateways, public IPv4 addresses | They dominate small and non-production environments |
| Observability | GB ingested, retained, queried; active series | Logs, metrics, traces | High-cardinality metrics and debug logs in production |
| Third-party SaaS | Per transaction or per seat | Payment fees (a percentage of the ticket price), e-mail per thousand | Often larger than the whole infrastructure β but a business cost |
Pricing models
| Model | Discount vs on-demand | Commitment | Good for |
|---|---|---|---|
| On-demand | β | None | Spiky and unknown workloads; the reference price |
| Commitments (AWS Savings Plans / Reserved Instances, GCP committed use discounts, Azure reservations) | Substantial (often tens of percent) for 1 or 3 years | Pay for a baseline whether you use it or not | The steady baseline: the 6 always-on nodes, the databases |
| Spot / preemptible | Large, variable | None, but the provider can reclaim the capacity with short notice (two minutes on AWS) | Stateless, interruptible work: workers, batch, CI runners, part of the stateless fleet |
| Serverless / per request | β | None | Low or very spiky traffic; scale to zero |
The usual portfolio is commit for the floor, on-demand for the daily wave, spot for the interruptible part β which only works if you know your baseline, i.e., after the Inform phase.
A cost model of BoxOffice
The model below estimates the monthly bill of the production architecture designed in this course. Every number is an assumption you can change β that is the point of a model: it makes the drivers explicit and lets you ask "what if?". Prices are approximate on-demand list prices for AWS us-east-1; confirm them with the provider's calculator before quoting them, and remember that other regions (including sa-east-1, SΓ£o Paulo) are more expensive.
Assumptions
- Production only (staging and load-test environments are extra), running 24/7 (730 h per month).
- Steady state: 6 worker nodes run all services. Four on-sales per month, each needing 24 extra nodes for 2 hours.
- Not included: payment-provider fees, taxes, support plans, free tiers and discounts, cross-AZ transfer between services.
| Component | Palette | Sizing (assumption) | Steady $/month | On-sale extra | Share |
|---|---|---|---|---|---|
| Kubernetes control plane | β | 1 cluster | $73 | $0 | 1% |
| Worker nodes (services) | app | 6 Γ m6i.xlarge, +24 at peak | $841 | $37 | 17% |
| PostgreSQL primary, Multi-AZ | sql | db.r6g.large Γ2 + 2 Γ 200 GB | $374 | $0 | 7% |
| PostgreSQL read replica | sql | db.r6g.large + 200 GB | $187 | $0 | 4% |
| Redis (primary + replica) | cache | 2 Γ cache.r6g.large | $301 | $0 | 6% |
| Kafka (managed) | kafka | 3 brokers + 3 Γ 500 GB | $610 | $0 | 12% |
| Load balancer | lb | 1 ALB, ~5 LCU (Γ20 at peak) | $46 | $6 | 1% |
| NAT gateways | nat | 3 AZs + 2 TB processed | $189 | $0 | 4% |
| Object storage | objstore | 500 GB tickets and images | $12 | $0 | 0% |
| CDN egress | cdn | 20 TB/month | $1,700 | $0 | 34% |
| Logs ingestion | logs | 1 TB/month | $500 | $0 | 10% |
| Serverless thumbnails | serverless | 2 M runs Γ 0.5 s Γ 1 GB | $17 | $0 | 0% |
| Transactional e-mail | email | 1.5 M e-mails | $150 | $0 | 3% |
| Total | $4,999 | $43 | 100% |
Total β $5,042/month. The four on-sales add only $43 because the 24 extra nodes exist for 8 hours; keeping them 24/7 'just in case' would add β $3,364/month. At 400,000 tickets per month the platform costs β $0.013 per ticket sold.
Reading the model
- The peak is cheap; the baseline is not. Elastic capacity for the on-sales costs almost nothing because it exists for eight hours. Provisioning for the peak permanently would cost more than the entire rest of the compute bill. This is the economic argument for autoscaling, pre-scaling and waiting rooms (Capacity planning).
- Data services dominate compute. PostgreSQL, Redis and Kafka together cost more than the nodes running nine services. Before adding a fourth data store, ask whether an existing one can do the job.
- Bytes cost more than CPUs. CDN egress and log ingestion are two of the largest lines. Image compression, longer cache lifetimes, sampling debug logs and shorter retention change the bill more than tuning Java.
- Unit economics turns the bill into a business metric: cost per ticket sold (or per 1 000 requests, per active user). A bill that doubles while tickets triple is good news; a bill that grows while tickets are flat is a regression β track it like a performance regression.
Pay per request or pay per hour?
When is FaaS cheaper than always-on containers? The plot compares a function (100 ms per request at 512 MB, behind an HTTP API front door) with a minimum of two small container tasks behind a load balancer, each serving 50 requests per second:
Below roughly ten requests per second on average, paying per request wins β idle containers are pure waste. Above it, the per-request premium accumulates and always-on containers win, more so with commitments. Spiky traffic shifts the picture: what matters is the average, while containers must be sized for the peak (or autoscale). This is why BoxOffice runs the thumbnail generator and webhook receivers as functions and the purchase path as containers.
Optimization levers
The levers below follow the cost-optimization practices of the cloud providers' well-architected frameworks3. Each one saves money and each one, except the last, can hurt an SLO if applied blindly.
| Lever | What to do | Typical effect | Risk to the SLOs |
|---|---|---|---|
| Rightsizing | Set requests from measured usage (e.g. p95 of CPU and max of memory over two weeks), not guesses | Fewer nodes for the same pods | Too tight β throttling and OOMKilled; re-run the load test |
| Autoscaling and scale to zero | HPA + node autoscaling; KEDA scale-to-zero for workers; turn off staging at night and weekends | Non-production hours: 730 β ~250 per month | Cold starts; scale-up lag at peaks |
| Commitments | Cover the measured baseline with 1-year commitments | Lower unit price on the floor | Paying for unused commitment if the architecture changes |
| Spot capacity | Run workers, CI and a share of stateless replicas on spot, spread across instance types | Large discount on that share | Interruptions: needs graceful shutdown, PDBs, on-demand fallback |
| ARM instances | Graviton/Axion/Ampere nodes for JVM and Python services (multi-arch images) | Lower price per vCPU for similar performance on many workloads | Verify with a benchmark; native libraries must exist for ARM |
| Reduce data transfer | CDN with long TTLs; keep chatty services in the same AZ; VPC endpoints for S3/ECR instead of NAT | Lower egress and NAT processing | Zone affinity reduces spreading |
| Storage lifecycle | Move old ticket PDFs to infrequent-access/archive tiers; expire snapshots | Lower GB-month | Slower, priced retrieval |
| Observability hygiene | Sample traces, drop debug logs in production, cap metric cardinality, shorter retention | Lower ingestion bill | Less detail when debugging β keep exemplars and error traces |
| Delete idle resources | Unattached volumes, old load balancers, forgotten test clusters | Pure savings | None |
Cost and reliability pull in opposite directions
Multi-AZ doubles the database cost; a second region roughly doubles everything. Those are not waste β they buy availability. FinOps is not "spend less", it is "know what each dollar buys". Write the trade-off in the design document: "Multi-AZ PostgreSQL costs $187/month more and turns a zone failure from a multi-hour outage into a one-minute failover."
Estimating in a system-design interview
Interviewers rarely want dollars, but cost-awareness is a strong signal. A three-step habit is enough:
- Name the drivers: "Cost is dominated by egress for images (CDN), the data tier, and peak compute."
- Order of magnitude from your capacity estimate: "30 nodes for 8 hours a month is cheap; 30 nodes for 730 hours is not β so we autoscale and pre-scale."
- One optimization with its trade-off: "Spot for workers, because they are idempotent and retry; never for the primary database."
In the interview
Cost questions often come disguised: "Do we really need Kafka here?", "Why three replicas?", "Could this be serverless?". Answer with utilization and peak-to-average ratio: always-on capacity is cheapest when it is busy; per-request pricing is cheapest when traffic is low or very spiky. Mention that data transfer and managed data stores usually dominate before compute does.
Best practices
- Allocate the cluster bill by resource requests, per namespace and label β nodes are shared, so requests are the fair key, and they are what the scheduler reserves.
- Set a budget with alerts at 50%, 80% and 100%, plus an anomaly alert on daily spend β a forgotten load-test cluster shows up in a day, not on the invoice.
- Put the cost of a design change in its design document, in dollars per month β "Multi-AZ PostgreSQL: +$187/month, a zone failure becomes a one-minute failover" lets the reviewer judge the trade-off. In BoxOffice: optimization levers.
- Commit only up to the lowest usage of the last months β commitments are paid whether used or not; the floor is the only safe part. In BoxOffice: the 6 always-on nodes, not the 24 on-sale nodes (cost model).
- Give each telemetry signal its own retention β error traces and SLO metrics for months, debug logs for days; ingestion and retention are among the largest lines of the bill.
Pitfalls
- No tags: costs that cannot be attributed cannot be owned or optimized.
- Estimating compute only: the fixed hourly items, data transfer and observability are where surprises come from.
- Rightsizing without a load test: the savings show up in the bill, the throttling shows up in the p99 at the next peak.
- Commitments before measuring the baseline: locking in capacity for an architecture you are about to change.
- Non-production environments running 24/7: often a large share of the total bill.
- Optimizing cents while the NAT gateway processes terabytes: sort by cost before optimizing.
How it connects
flowchart LR
BILL["the bill: compute, managed services,<br/>storage, transfer, fixed items"] --> INF["Inform: tags,<br/>cost by pod requests"]
INF --> OPT["Optimize: rightsizing,<br/>commitments, spot, ARM"]
OPT --> OPS["Operate: budgets,<br/>anomalies, monthly review"]
BASE["baseline vs peak<br/>β autoscaling, pre-scaling"] --> OPT
FAAS["per request vs per hour<br/>β serverless break-even"] --> OPT
UNIT["unit economics:<br/>cost per ticket sold"] --> OPS
OPT --> SLO["every lever can hurt an SLO<br/>β Reliability"]
OPT --> LT["re-run the load test<br/>β Performance"]
BILL --> MAZ["Multi-AZ, second region:<br/>cost that buys availability"]
BILL --> OBS["observability bill:<br/>cardinality, retention"] Palette components: sql Β· cache Β· kafka (the data tier dominates compute) Β· cdn (egress) Β· nat (fixed hourly + per GB processed) Β· objstore (lifecycle tiers) Β· logs (ingestion) Β· serverless Β· worker (spot candidates) Β· payment (fees, a business cost). Scenarios: serverless-burst, multi-region (what a second region costs). Continues in: Compute β containers or serverless, Performance β capacity planning, Reliability β disaster recovery, Network & security β the cloud network (NAT gateways), Observability β signals (cardinality), Project β cost report.
-
FinOps Foundation. FinOps Framework.Β β©β©
-
STORMENT, J. R.; FULLER, M. Cloud FinOps. 2nd ed. O'Reilly, 2023.Β β©
-
AWS. Well-Architected Framework β Cost Optimization Pillar.Β β©
-
OpenCost. Open source cost monitoring for cloud-native environments (CNCF project).Β β©