Chapter 6 of 6 · 16 min read
Gross margin per tenant
On this page
Gross margin on an account is what is left of the subscription once the direct cost of serving it is taken out, and for a product that calls a paid model on a customer's behalf it is a number that can go negative without anything failing. Cost attribution per tenant, the subject of topic 2.16 on tracking cost per request, user, feature and tenant, produces the numerator; the plan price is sitting in the billing system; and the ratio between them is one query. Very few teams run it before a quarterly margin review forces the question, which is why the first time an unprofitable account is seen it has usually been unprofitable for months.
This chapter is about that ratio and what follows from it. The case it works through is a customer doing nothing wrong at all — no payload, no restart loop, no bug — whose ordinary daily use generated several times their subscription in model spend. The controls that would have bounded them are the same three scopes that bound an attacker, and the reason the case belongs in a topic about denial-of-wallet is that the system cannot tell the two apart and does not need to.
Intent is not a control
The OWASP entry for unbounded consumption defines the weakness by the absence of ceilings on resource use rather than by the presence of an adversary. That framing is the useful one, because your system cannot distinguish a firm uploading its archive every morning from a script uploading the same archive every morning, the provider bills identically for both, and every mechanism that would have bounded one would have bounded the other.
| A night of nine hundred restarts | Vantorr Legal, a fourteen-seat practice | |
|---|---|---|
| Trigger | deliberate restarts | a scheduled integration |
| Cost curve | €4,005 in four hours | €3,100 over a month |
| Controls that would bound it | per-task, per-user, per-tenant ceilings | the same three |
| First signal available | iteration count, then per-user daily spend | per-tenant spend against plan price |
| Time to detection | none, discovered by accident | five weeks, discovered by accident |
| Remediation | stop the account | change the workload, the cost, or the price |
| Who owns it | engineering | engineering and product |
Five of those seven rows are identical. The rows that differ are the last two, which is exactly the part that gets handed to product, and the rows that match are the part that gets handed along with it and then dropped. If an account like this leaves the meeting as a pricing ticket, the next event of this shape — which will be an attack, because attacks are the version of this that someone is optimising — arrives at a system with the same absent ceilings and the same five-week detection lag.
There is one asymmetry worth keeping hold of, because it changes what you monitor rather than what you build. An attacker searches for the worst case deliberately and will find the exact combination of inputs that maximises your cost, so the ceiling has to hold against a hostile search. A customer arrives at an expensive pattern by accident and then repeats it every day, which means their cost is high, stable and predictable — and predictable is what makes a threshold on the rate of change useful for one and useless for the other. A tenant whose daily spend triples in an hour is worth interrupting; a tenant whose daily spend has been flat at €103 for five weeks is worth a conversation. Both need the absolute ceiling underneath.
The ratio that should have existed
The week's work when a row like that surfaces is detection and a backstop, not enforcement against a happy paying customer, because breaking Vantorr on Wednesday to prove a point about ceilings is a bad trade. The alert that should have existed is one query against the attribution tables, and it costs an afternoon:
SELECT
t.tenant_id,
t.name,
t.seats,
round(t.plan_eur_month, 2) AS revenue,
round(sum(c.cost_eur), 2) AS model_cost,
round(sum(c.cost_eur) / nullif(t.plan_eur_month, 0), 2) AS cost_ratio,
round(sum(c.cost_eur) / nullif(t.seats, 0), 2) AS cost_per_seat
FROM tenants t
JOIN cost_events c ON c.tenant_id = t.tenant_id
WHERE c.occurred_at >= date_trunc('month', now())
GROUP BY t.tenant_id, t.name, t.seats, t.plan_eur_month
HAVING sum(c.cost_eur) / nullif(t.plan_eur_month, 0) > 0.35
ORDER BY cost_ratio DESC;Run it and you will find more than one row; on the system in question there
were six above 0.35 and two above 1.0. Alert on cost_ratio crossing 0.35
month-to-date, which gives roughly three weeks of warning at the observed burn,
and page nobody — this is a daily digest to a channel where an engineer and
someone from product both read it.
The two thresholds in that paragraph are not arbitrary, and knowing where they
come from is what lets you move them for your own margins. cost_ratio is the
month's direct model spend for a tenant divided by what that tenant pays for the
month, so it is expressed in units of the subscription. One is therefore not a
threshold anybody chose: it is the line where the direct cost of serving an
account equals its entire revenue, above which the gross margin on that account
is negative before a single other cost is counted. Two accounts sat above it. The
0.35 is a choice, and the reasoning behind it is the distance to the line rather
than the number itself — a third of the subscription consumed by one variable
cost leaves room to notice, investigate and act before the account crosses into
loss. An account burning at several times its plan price crosses a third of it
within the first days of the month, which is where the weeks of warning come
from, and an account drifting up slowly crosses it months before it would have
reached one. The same threshold gives useful notice in both cases because it is
placed relative to break-even rather than relative to an amount of money.
Alongside the alert, set a per-tenant absolute ceiling far above any plausible legitimate use, at something like three times the plan price, with the consequence being an alert and an operator decision rather than a hard refusal. That is deliberately a cap that fires roughly never for legitimate accounts, and unlike a percentile-derived ceiling it is justified by an argument about what the business can absorb rather than by the shape of last month's distribution.
The cheapest lever available in the same week is not a control at all. It is a page in the product that shows a tenant what they consumed, in units they understand — documents indexed, pages processed, agent runs — with the previous day, the month to date, and their plan's expected range beside it. Publishing consumption has a second effect that matters when caps arrive later: a limit that appears next to a number the customer has been watching for two months reads as a policy, while a limit that appears with no prior visibility reads as a service degradation. If you intend to enforce tenant ceilings this quarter, ship the visibility first, and put the ceiling on the same page as the number it applies to.
What a quarter of engineering moves
Four structural changes are worth pricing, and the order matters because the cheapest one is also the largest.
| Change | Mechanism | Monthly cost after |
|---|---|---|
| baseline | — | €3,100 |
| prompt caching on the stable prefix | system prompt, tool schemas and matter context billed at ~10% on cache reads | €1,302 |
| content-hash deduplication | skip re-indexing documents whose hash is unchanged | €899 |
| tier routing | extraction and classification hops on the small model, synthesis on the frontier model | €701 |
| transcript compaction and iteration caps | bound the growth described earlier in this topic | €533 |
That table lists levels and the teaching is in the differences, so take it as four subtractions rather than five numbers. Prompt caching removes the largest step by a wide margin — close to seven-tenths of everything that came off the account across the whole quarter. Deduplication removes the next largest, tier routing a smaller one, and the compaction and iteration ceilings the smallest of the four. Add the four decrements and they come to exactly the distance from €3,100 to €533, with nothing unaccounted for, which is the check that says these four changes are the whole of what engineering did and there is no fifth contribution hiding in the ordering. That closure is what makes the last row's smallness safe to state plainly instead of defensively: the controls from this topic really did contribute the least in euros, and the reason to keep them is not their share of this table.
Prompt caching did the heavy lifting because this workload has a large stable prefix reread on every call, which is the ideal case for it, and because cached input is priced around a tenth of ordinary input. Deduplication was pure waste removal once a customer conversation revealed a daily re-upload. Tier routing required measuring quality on each hop and accepting the small model where the task was extraction rather than judgement. Compaction and the per-task ceilings are the controls from this topic, and they contributed the least in euros while being the only ones that bound the worst case rather than the average.
Two of those rows are worth taking one step further, because each carries a condition that decides whether it transfers to your account or not. Prompt caching works by the provider keeping the processed form of a prefix and charging a reduced rate when the next call presents the same leading bytes. So its saving is the share of the bill that is prefix rereads, multiplied by the difference between the ordinary and the cached rate. This workload has a large system prompt, a large tool schema and a matter context, all identical from call to call, and a partner asking questions all day reissues them constantly — which is why the share is large here. Change any of those and the row collapses: a prompt that interpolates the current time near the top, a tool list that varies with the user, a retrieved passage inserted before the stable material, and the prefix stops matching, so nothing is cached and the row is worth nothing. The saving is not a property of caching being switched on. It is a property of the prompt being built so that its front never moves.
Deduplication is the row people most often misread, and the misreading is a confusion between two different denominators. The great majority of the documents processed that month had been processed before, unchanged, and skipping them removed under a third of the bill it was applied to. Those two proportions disagree because a document costs money twice for different reasons. Indexing it is paid once per upload and is what deduplication removes. Answering questions over it is paid every time somebody asks, and the partners asked all day, so that part of the bill is unaffected by whether the document was new. A share of artefacts is not a share of euros, and the gap between them tells you which of the two activities the account is actually paying for.
That ordering is the argument against treating cost as a purely commercial problem, and also the argument against treating it as a purely technical one. Engineering moved the account from €3,100 to €533 against €400 of revenue, a cost ratio of 7.75 down to 1.33, and could not get it below 1.0. The remaining gap is a pricing gap, and it is now small enough that product can reason about it with real numbers rather than about an outlier nobody understands.
When per-seat pricing survives
Seat pricing works when the cost a seat generates is bounded by something other than your goodwill, and the usual bound is a human being. A seat that only spends money when a person is typing has a natural ceiling around what one person can consume in a working day, so the cost distribution across seats is tight and a single price covers it with a predictable margin.
| Regime | Cost per seat, p50 → p99 | Seat pricing | What to do instead |
|---|---|---|---|
| Human-paced, small contexts | €2 → €7 | works | nothing |
| Human-paced, large documents | €6 → €60 | works with caps | seat price set against p95, cap at p99.5 |
| Mixed interactive and scheduled | €4 → €180 | fragile | seat + included usage, overage per unit |
| Unattended agents, customer-scheduled | €10 → €900 | fails | usage or capacity pricing |
Overage, in the third row, means a fixed allowance included in the seat price together with a per-unit charge for whatever a customer runs beyond it. The diagnostic worth computing is the ratio of p99 cost per seat to median cost per seat. Below about 5 the mean is a usable basis for a price. Above 10 the top percentile of accounts consumes more than the entire margin pool from everyone else, so every new customer is a coin flip on whether they are profitable, and pricing that averages over the distribution transfers money from ordinary customers to heavy ones until the heavy ones dominate the mix.
Work that ratio out for each row and the verdict column stops being an opinion. The human-paced small-context row gives three and a half, below the threshold where the mean is a usable basis for a price, and the verdict is that seat pricing works. The large-document row gives ten, sitting exactly on the boundary, and the verdict is that it works with caps. The mixed row gives forty-five and the unattended row gives ninety, both far past the threshold, and the verdicts are fragile and fails. Four rows, one division each, and the column reproduces itself — which means the verdict is a statement about the spread of the cost distribution and not about the industry the customer is in.
The other thing that table shows, read across rather than down, is why nobody notices the problem from ordinary customers. The median column barely moves between the four regimes: a typical seat costs a similar handful of euros whether the product is human-paced chat or unattended overnight pipelines. The p99 column spans from single euros to hundreds. So the account most of your customers resemble looks the same in every regime, and everything that decides whether the price works is in the part of the distribution nobody looks at unless they go looking. An average over seats is exactly the operation that discards it.
That ratio is not fixed by the market. It is fixed by your caps, which is the thing worth saying to product directly: a per-tenant ceiling converts an unbounded cost per seat into a bounded one, and a bounded cost per seat is what makes a seat price computable at all. Vantorr's ratio before the quarter's work was 31; afterwards it was 6, and the difference between those two numbers is the difference between a price you can defend and a wager.
The argument against keeping seat pricing gets strong in one specific case, which is when the product's value is delivered by unattended work the customer schedules. At that point seats measure nothing relevant, because a firm with four seats and a nightly pipeline over ten thousand documents costs more than a hundred-seat firm doing interactive work, and no cap you can set is both generous enough for the first and meaningful for the second. Products that reach that shape end up with a usage component, whether it is credits, metered tasks, or a capacity tier that names a concurrency and a monthly volume.
Measuring before enforcing
The sequence that gets this in place without a large project, in the order that yields the most per week of effort, is to measure first. Compute the per-request cost distribution and the per-tenant cost ratio, since both come from attribution you probably already have and both produce numbers that make every subsequent argument shorter. Add the in-loop counters next, running in observe-only mode for a week so you can see what the thresholds would have caught and what they would have broken. Then enable the per-task ceiling, then the per-user daily ceiling, then the tenant ceiling, each with the consequence written down before the number is.
Expect the first enforcement week to surface a legitimate workload you did not know about. That is the control working, and it is why the boundary behaviour should start as queue-and-notify rather than kill; a cap you can leave on is worth more than a stricter one you turn off on Thursday. The synthetic that deliberately trips each ceiling belongs in the same deploy as the ceiling itself, because the failure this whole topic is about is not a limit set slightly too high — it is a limit that quietly never applies to anything.
An expensive customer and an attacker are the same finding arriving through different doors, and the ratio of cost to revenue per tenant is what makes either of them visible before an invoice does. Nothing in that ratio depends on intent, which is the property that makes it worth building: it fires on a scheduled integration nobody remembers configuring, on a compounding payload in an uploaded document, and on a bug in your own retry wrapper, using the same query and the same threshold.
What it does not do is resolve the account. Engineering can move a cost ratio a long way — 7.75 to 1.33 here — and the remainder is then a commercial decision about price, plan shape or which customers a product wants. A cap with no price behind it protects margin by degrading a customer who is paying you, and a price with no cap behind it is a number set against a cost nobody has bounded.
Denial-of-wallet — read the current version at https://daviddominguez.dev/learn/en/denial-of-wallet/gross-margin-per-tenant