Design problems
AI-era systemsMediumSDE-2 30 min

Design a cost-per-request FinOps dashboard for AI features

Asked at
AtlassianFreshworksRazorpaySalesforceSwiggyZoho

The interviewer asks

“Our B2B SaaS has added AI features (ticket summaries, reply drafts, an agent) and the LLM bill tripled in a quarter. Design a system that tells us how much each tenant, feature and request costs us, today, including shared infrastructure, and alerts us when cost jumps unexpectedly.”

What a strong answer looks like

A strong answer separates the exact part from the allocated part. LLM cost is directly attributable: every call already passes through the AI gateway, which emits a usage event with tenant, feature, model and the provider-reported token counts; cost is computed with a versioned price book (prices change, so history must use the price valid at call time), including cached-token and batch discounts. Shared infrastructure (Kubernetes, databases, vector store, Kafka) comes from the cloud cost export and OpenCost and is allocated to tenants by a documented usage share, with idle cost shown separately. Events flow through Kafka into ClickHouse, with materialised rollups for fast dashboards; per-tenant cost never goes into Prometheus labels. Anomaly detection runs at two speeds: a streaming check that catches a runaway agent loop within minutes (and can throttle it through the gateway budget), and a daily seasonal baseline. Strong candidates insist on tag propagation through async hops, cost per outcome (per resolved ticket) not just per call, reconciliation against the provider invoice, and margin per tenant against plan price.

Concepts to mention

direct (exact) vs shared (allocated) costgateway as single source of LLM usageprovider-reported token usage incl. cached and reasoning tokensversioned price book with effective datestag propagation through async hops (baggage, Kafka headers)ClickHouse with materialised rollups, not Prometheus labelsCUR amortised cost + OpenCost allocation by usage keyscost per successful outcome and margin per tenanttwo-speed anomaly detection with absolute floorsinvoice reconciliation

1 Requirements

Functional

  • Cost by tenant, feature, endpoint, model and team, for any time range, down to a single request
  • LLM cost (exact) and shared infra cost (allocated), shown separately and combined
  • Unit economics: cost per request, per active user, per successful outcome (resolved ticket, accepted draft)
  • Gross margin per tenant: plan revenue minus attributed cost
  • Budgets per feature and per tenant with alerts at 80% and 100%; optional hard stop via the gateway
  • Anomaly alerts routed to the owning team with a link to the offending tenant/feature/prompt version
  • Monthly reconciliation of computed LLM cost against provider invoices

Non-functional

  • LLM cost visible within 5 minutes of the call; infra allocation daily (cloud billing data lags 8-24 h)
  • Runaway spend detected within 15 minutes
  • Computed LLM cost within 2% of the provider invoice each month
  • Attribution coverage: ≥ 98% of LLM spend tagged with tenant and feature
  • Dashboard queries p95 < 3 s over 13 months of data
  • Scale: 40M LLM calls/day across 5,000 tenants and ~30 features

Out of scope (say it out loud)

  • Customer-facing invoicing and usage-based billing (related but separate system)
  • Choosing which model each feature uses (the dashboard informs it)
  • Non-engineering costs (salaries, office)

2 Estimates

Usage events~460/s avg, ~2k/s peak40M LLM calls/day ÷ 86,400 ≈ 460/s; business-hours peak in IST ≈ 4×
Usage data volume~40 GB/day raw, ~4 GB/day stored~1 KB JSON per event (ids, tags, token counts, cost, latency) × 40M = 40 GB/day; ClickHouse columnar compression ~10× → ~4 GB/day, ~1.5 TB for 13 months
LLM spend~$20k/day (illustrative)Assume 40M calls × ~1,000 input tokens = 40B tokens × ~$0.30 per 1M blended (after cache discounts) ≈ $12k, plus 40M × ~200 output tokens = 8B × ~$1.20 per 1M ≈ $9.6k → ~$22k/day. A 1% error is ~$200 a day, ~$6k a month
Cloud billing rows~5-20M line items/dayAWS Cost and Usage Report at hourly granularity with resource IDs: thousands of resources × 24 hours × usage types; loaded daily from S3
Rollup sizes~3.6M rows/day for tenant×feature×model×hour5,000 tenants × 30 features × ~1 model active × 24 hours ≈ 3.6M upper bound; actual far less because most tenants use a few features. Dashboards read rollups, not raw events
Anomaly checks~150k series every 5 minutes5,000 tenants × ~30 features ≈ 150k (tenant, feature) series; only a few thousand are active in any window, so a streaming job handles it easily

3 API

EndpointWhat it does
GET/v1/costs?group_by=tenant,feature&from=&to=&granularity=hourAggregated cost (LLM, infra, total) from ClickHouse rollups; powers dashboards and exports.
GET/v1/costs/requests/{request_id}All LLM calls and allocated infra for one user request or agent run, with links to traces.
GET/v1/unit-economics?feature=ticket-summary&outcome=resolvedCost per outcome: total cost ÷ outcome events joined on root request id.
PUT/v1/budgets/{scope}Set a budget for a feature or tenant: amount per day/month, alert thresholds, action (alert | throttle | block).
PUT/v1/prices/{provider}/{model}Add a new price version with effective_from; never edits old versions.
GET/v1/anomalies?status=openOpen anomalies with scope, expected vs actual spend, owner team and suspected cause (new prompt version, traffic, model change).

4 Data model

llm_usageClickHouse (MergeTree, partition by day)
event_idtsrequest_idroot_request_idtenant_idfeatureendpointteammodelproviderprompt_versioninput_tokenscached_input_tokenscache_write_tokensoutput_tokensreasoning_tokensbatch (bool)price_version_idcost_usdcost_inrstatusretry_of

One row per LLM call. ORDER BY (tenant_id, feature, ts). ReplacingMergeTree on event_id absorbs duplicate deliveries. TTL 13 months.

price_bookPostgres
price_version_idprovidermodeleffective_fromeffective_toinput_per_1mcached_input_per_1mcache_write_per_1moutput_per_1mbatch_discount_pctcurrency

Versioned, append-only. Cost is computed with the version valid at the call's timestamp, and the version id is stored on each row so recomputation is auditable.

fx_ratesPostgres
dateusd_inrsource

Providers bill in USD; finance reports in INR. Store the daily rate used so numbers do not shift later.

infra_cost_dailyClickHouse (loaded from AWS CUR in S3 + OpenCost)
dateservicenamespaceresource_typeamortised_costidle_costtags

Amortised cost (reserved instances and savings plans spread across usage) is the right basis for unit economics.

allocation_keysClickHouse (from traces and app metrics)
dateservicetenant_idcpu_secondsrequestsstorage_bytesvector_count

Usage shares used to split each shared service's cost among tenants. Method stored per service and versioned.

cost_rollupsClickHouse materialised views (SummingMergeTree)
hourtenant_idfeaturemodelcallsinput_tokensoutput_tokenscost_usd

Updated on insert; dashboards read these for sub-second queries over months.

budgets / anomaliesPostgres
scopeperiodamountthresholdsactionowner_team; anomaly_idscopewindowexpectedactualstatusack_by

Small, transactional; budget actions are pushed to the AI gateway's limit config.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALLLM call + tagsprovider callusage eventconsumeprice at tscosted rows5-min windowsspend vs baselinealert ownerthrottle / blockCUR + OpenCostallocated infraqueriesrollupsEng / financedashboardsProduct servicesAI gateway (usage)Cost API + dashboardsBudget + anomalyservicePricing + enrichworkersKafka: llm_usageDaily allocation jobStreaming spendwindowsPostgres prices,budgetsClickHouse + rollupsS3: CUR exportsLLM providersSlack / PagerDuty
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

From one LLM call to the dashboard

  1. 1The ticket service calls the AI gateway with headers X-Tenant, X-Feature, X-Request-Root (propagated from the user's original request via OpenTelemetry baggage, through Kafka headers and Temporal workflow context).
  2. 2The gateway calls the provider and reads the provider-reported usage block (input, cached input, output tokens) from the final response or stream chunk.
  3. 3It emits a usage event to Kafka keyed by tenant_id; calls with missing tags are still recorded under 'unattributed' with the calling service name, and a coverage metric drops.
  4. 4Enrichment workers look up the price version valid at the call timestamp (cached in memory, refreshed on change), compute cost_usd and cost_inr, and batch-insert into ClickHouse.
  5. 5Materialised views update hourly rollups by tenant, feature and model as rows arrive.
  6. 6Within ~2-5 minutes the dashboard shows the call in 'today so far' and in the per-request drill-down.

A runaway agent loop at 2 a.m.

  1. 1A bug makes the reply-draft agent retry the same tool call in a loop for one large tenant; its spend in 5-minute windows jumps from ₹150 to ₹6,000.
  2. 2The streaming job compares each (tenant, feature) window with its baseline for that hour of the week; the jump exceeds both 5× baseline and the absolute floor (₹2,000 per window), so an anomaly opens.
  3. 3The budget service checks the feature's policy: action = throttle on anomaly. It pushes a temporary token budget for (tenant, reply-draft) to the gateway, which starts returning 429 for that pair only.
  4. 4The on-call engineer of the owning team is paged with the tenant, feature, prompt version, top request ids and a trace link; most loops are visible in the trace in seconds.
  5. 5After the fix, the engineer releases the throttle; the anomaly record stores cause and cost impact for the weekly review.

Daily shared-cost allocation

  1. 1At 10:00 IST the allocation job loads yesterday's CUR line items (amortised cost) and OpenCost namespace costs from S3.
  2. 2Each shared service's cost is split by its allocation key: the RAG retrieval service by query count per tenant, the vector DB by vectors stored, Postgres by storage plus query time from traces.
  3. 3Idle capacity (requested but unused) is reported as a separate line per team, not hidden in tenants' costs.
  4. 4Results are written to ClickHouse; the dashboard shows total cost per tenant and margin against plan revenue.

7 Deep dives

Getting LLM cost exactly right

LLM cost looks simple (tokens × price) but is easy to get wrong by 10-30%. Use the provider-reported usage, not your own token estimate; tokenisers differ and providers bill on their count. Price each token class separately: cached input tokens are discounted heavily by most providers, some providers charge a premium for writing to the prompt cache, reasoning tokens are billed as output even though the user never sees them, and batch APIs are typically about 50% cheaper. Calls that failed after partial output, cancelled streams and retries still cost money, so record them with status and retry_of so retry waste is visible. Prices change, so the price book is versioned with effective dates and each row stores the price_version_id used; recomputing last month with today's price is a classic bug. Currency: providers bill in USD, so store the daily USD-INR rate used. Finally, contract discounts and credits apply at invoice level, so show 'list cost' per call and apply a monthly true-up factor for 'effective cost', and reconcile computed totals against the invoice every month; a gap above 2% usually means untracked traffic bypassing the gateway or a missing price class.

Attribution: tags must survive every hop

Cost per tenant is only as good as the tags on each call. The gateway is the choke point: all LLM calls must go through it (block direct provider keys with network policy and by not handing them out). Tags come from the original request context: tenant, feature, endpoint and root request id travel as OpenTelemetry baggage, are copied into Kafka message headers by producers, into Temporal workflow memo or headers for durable agent runs, and into batch jobs as job metadata. The common failure is the async hop: a summary generated by a background worker loses the tenant because nobody copied the header. Make this measurable: untagged calls are still recorded as 'unattributed' with the calling service, attribution coverage is a dashboard metric with an owner, and CI tests check that critical async paths carry the tags. For agents, one user request can trigger 20 LLM calls; the root_request_id ties them together so you can report cost per agent run and, by joining with outcome events (ticket resolved, draft accepted), cost per successful outcome, which is the number product managers actually need to price a feature.

Allocating shared infrastructure fairly enough

LLM tokens are often the largest line, but RAG features also use vector databases, embedding jobs, Postgres, Kafka and Kubernetes compute that serve all tenants together. Allocation is an estimate, so the goal is a method that is simple, documented and stable. Step one: get cost per service. The AWS Cost and Usage Report (amortised, so reserved instances and savings plans are spread fairly) gives cost per resource and tag; OpenCost or Kubecost splits node cost across namespaces and pods, usually by the larger of requested and used CPU and memory. Step two: split each service's cost across tenants by a usage key that tracks what drives its cost: queries or CPU-seconds from traces for a stateless service, vectors stored for the vector DB, bytes stored for object storage. Step three: show what cannot be fairly allocated, such as idle capacity and platform overhead (observability, CI), as separate lines rather than smearing them across tenants, so teams see their waste. Version the allocation method so a change does not silently rewrite last quarter's margins. Billing data lags by up to a day, so infra cost is daily while LLM cost is near real-time; the dashboard labels which numbers are final.

Anomaly detection at two speeds

Different problems need different detectors. Slow drift (a new prompt version is 30% longer; a big tenant onboarded) shows in daily data: compare today's cost per (tenant, feature) with the trailing 14 days for the same weekday and alert when it is more than ~3 standard deviations above the mean or more than 2× yesterday. Sudden runaways (an agent loop, a retry storm, a scraper abusing a free-tier tenant) burn money in minutes and cannot wait for a daily job. A streaming job (Flink, or ClickHouse materialised views queried every minute) keeps 5-minute spend windows per (tenant, feature) and compares them with the same hour-of-week baseline. To avoid alert fatigue, require both a relative jump and an absolute floor (a tenant going from ₹2 to ₹20 is not an incident), group alerts by owning team using a feature-to-team mapping, and attach the likely cause: changes in call count, tokens per call, model mix or prompt version. For features that opt in, the anomaly can act: push a temporary budget to the gateway so that one tenant-feature pair gets 429s while everyone else is unaffected. This turns the dashboard from a report into a control system.

8 Trade-offs

ClickHouse vs Prometheus for cost data

Prometheus is great for service metrics but each unique label set is a time series; 5,000 tenants × 30 features × models creates hundreds of thousands of series and kills it. ClickHouse handles high-cardinality event data and arbitrary group-bys cheaply. Keep only low-cardinality totals (spend per feature) in Prometheus for alerting next to service SLOs.

Compute cost at ingest vs at query time

Computing at ingest makes queries simple and fast and freezes the price used. Computing at query time from tokens allows re-pricing scenarios ('what if we moved to model B?'). Do both: store cost and price_version_id at ingest, and keep token counts so what-if analysis can re-price on demand.

Exact allocation vs simple allocation

Per-request CPU tracing can allocate infra almost exactly but costs engineering effort and its own compute. A few well-chosen usage keys per service get within a few percent, which is enough for pricing decisions. Spend precision where the money is: LLM cost exact, infra allocated.

Alert only vs automatic throttling

Auto-throttling stops runaway spend fast but can hurt a paying customer during a legitimate spike. Make it opt-in per feature, scope it to one tenant-feature pair, and require an absolute floor and a human-visible reason.

9 Bottlenecks

Ingest at 10× (400M calls/day)

Kafka absorbs bursts; ClickHouse needs larger batch inserts and more shards keyed by tenant hash. Rollups keep dashboard queries fast regardless of raw size; raw rows can be downsampled to hourly after 90 days.

Dashboard queries over raw data

Ad-hoc queries on billions of raw rows can be slow and expensive. Route standard views to materialised rollups, put query time limits on raw access, and precompute top-N tenants and features daily.

10 Failure handling

Gateway stops emitting usage events

The gateway writes usage to a local disk buffer if Kafka is unavailable and replays later; event_id dedupes. A freshness alert fires if no events arrive for a feature that normally has traffic. The monthly invoice reconciliation is the final safety net.

Wrong price entered

A typo makes a model 10× more expensive. Price changes go through review, are validated against the provider's published price, and because rows store price_version_id, affected rows can be recomputed precisely after the fix.

Traffic bypassing the gateway

A team calls a provider directly with its own key: the invoice is higher than computed cost. Reconciliation by API key shows the gap; prevent it with egress network policy and central key management.

11 What impresses

  • Separates exact LLM cost from allocated infra cost and shows idle cost separately
  • Knows the pricing traps: cached tokens, cache writes, reasoning tokens, batch discounts, failed calls, FX
  • Versions prices and allocation methods so history is stable and auditable
  • Reports cost per outcome, not only per call
  • Turns anomaly detection into a control loop through gateway budgets, scoped to one tenant-feature
  • Reconciles against the invoice and treats the gap as a signal

12 Mistakes

  • Estimating tokens client-side instead of using provider usage
  • Recomputing old costs with today's prices
  • Per-tenant labels in Prometheus
  • Losing tenant tags in background jobs, leaving a large 'unknown' bucket
  • Smearing idle and platform cost across tenants silently
  • Only daily anomaly checks, so a runaway loop burns a day's budget

13 Follow-ups

Q1How would you use this data to decide which feature should move to a cheaper model?
Q2How do you show a customer their own AI usage without exposing internal costs?
Q3How do you allocate the cost of a shared semantic cache that saves money for everyone?
Q4How would you forecast next month's LLM bill?
Q5A tenant is margin-negative. What options does the data give you?
Q6How do you attribute GPU cost if you self-host models?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free