Skip to content

18 min read

Fixed and variable costs of a boundary

On this page

A build-versus-share decision reaches most rooms as two build estimates and a comparison. Four product teams each need billing data for their agents; each would spend about two weeks integrating with the provider directly; four times two is eight; the shared MCP server is six; eight beats six. The arithmetic is not where that model fails. It fails one line earlier, where a decision with a permanent operating obligation on one side and a contingent one on the other has been reduced to the cost of writing the code once.

Two distinctions do most of the work of fixing it. A cost is fixed if it accrues whether or not anything happens, and variable if it is triggered by an event. It is one-time if you pay it once and recurring if it comes back every month or every quarter. Sorting a proposal's cost lines into those four quadrants before adding anything up is what makes the two sides comparable, because the two options do not sit in the same quadrants at all.

Sort the costs before you add them

Cost lineSideKindWhere the number comes from
Build the serversharedone-timethe team that will build it, not the proposer
Auth, per-consumer isolation, tenant scopingsharedone-time build, then rotationthe last service that was given a new credential path
Hostingsharedfixed recurringthe monthly bill for a comparable service already running
On-call and operatingsharedfixed recurringthe platform's incidents per service per quarter
Consumer supportsharedfixed recurring, scaled by consumersmeasured Slack and ticket load of an existing internal platform service
Version and deprecation coordinationsharedfixed recurringthe dual-serving windows from your versioning policy
Each team's client integrationsharedone-time per consumerusually written as zero, which is why it is on this list
Build each direct integrationduplicatedone-time per consumerfirst team's actual time, then less for the ones who copy
Update each integration when upstream changesduplicatedvariable recurringthe provider's changelog for the last 24 months
Behaviour drifting between copiesduplicatedvariable recurringyour own incident history for duplicated logic

Two of those lines are routinely absent from the slide and each of them is worth more than the difference it was meant to settle. Consumer support exists because four teams you do not manage will ask questions, hit errors they cannot debug, and want fields you did not expose, and none of that work is on anyone's roadmap. The client integration line exists because a consumer does not get the capability for free just because a server exists: they still configure a client, obtain a credential, handle the tool's failure modes, and test it, which the four teams in this example estimated at two days each.

The on-call line deserves more than a row, because it is the one that is genuinely new rather than merely relocated. Before the server exists, a billing integration that breaks does so inside a product service that already has a dashboard, an owner, an alert route and somebody who knows what its data means. Afterwards there is a process with its own uptime, which needs a page target, a runbook that says what to do when the provider is slow rather than down, an alert on the difference between "no tickets" and "no answer", and a person at three in the morning who can tell whether a billing figure is stale or wrong. The half day a month in the model below is the platform's own measured average across services that mostly behave; a service in its first quarter costs more than that, and a service wrapping a third-party provider inherits the provider's bad weeks.

The estimates on the duplicated side also need adjusting downward, which is the correction that goes against the argument this chapter is making and belongs in the model anyway. Four integrations do not cost four times the first one. The second team reads the first team's code, copies the request signing and the pagination handling, and lands in about 40% of the time; the third and fourth are cheaper still. The billing example measured the first integration at ten days and the followers at four, so the duplicated build is 22 days rather than the 40 the slide implied.

Taking that twenty-two apart is worth the two lines it costs, because a figure working against your own case is the one an opponent will check first. The first team pays ten days. The second, third and fourth pay four each, which is twelve between them, and ten plus twelve is twenty-two with nothing left over. Subtracting that from the forty the slide charged leaves eighteen days of work nobody was ever going to do — a discrepancy larger than the two weeks the slide's whole conclusion rested on. Getting the duplicated side honest matters more than getting the shared side complete, because a reviewer who finds one inflated line against the option they favour stops reading the rest.

The reason a build estimate stands in for the cost in the first place is not laziness, and treating it as laziness loses the argument. It is the only number that exists before the work starts, it is the only number a proposer can be asked to produce, and it is the only one both options can be quoted in on the same day. A comparison drawn from it is therefore a comparison taken at the single moment the two options resemble each other. From the day after, they diverge: one side has acquired an obligation that runs every month whether anybody calls it or not, and the other has acquired a liability that costs nothing at all until a provider changes something. Weighing them at the one instant when both are just unwritten code is how a permanent cost gets set against a contingent one and found equal.

The model as it would be presented

Everything below is denominated in engineer-days, meaning one engineer working for one day, and converted to money at a loaded rate — salary plus employment costs, tooling and overhead — of $900 a day, which is the figure the finance team uses for internal build-versus-buy comparisons at this company.

The slide and the model quote the same work in different units, and converting between them is what lets a reader check the second against the first. A working week is five days, so the four teams at two weeks each come to forty engineer-days, and the six-week server they were held up against comes to thirty. That thirty is carried across below as SERVER_BUILD, unchanged and undisputed: nothing in the model argues with the proposer's estimate of the build itself. The disagreement is entirely about the lines sitting underneath and beside it.

python
# All figures in engineer-days. Sources are named per line.
FIRST_INTEGRATION   = 10   # Payments team's actual time on the first one
FOLLOW_INTEGRATION  = 4    # measured: followers copy ~60% of the work
SERVER_BUILD        = 30   # proposer's 6 weeks, functionality only
SERVER_AUTHZ        = 10   # token verification + per-consumer tenant scoping,
                           # excluded from the proposer's estimate
CLIENT_ONBOARD      = 2    # per consumer: config, credential, error handling
HOSTING_PER_YEAR    = 2.4  # $180/mo at a loaded $900/day
ONCALL_PER_YEAR     = 6    # platform average: 0.5 day/month/service
VERSIONING_PER_YEAR = 4    # dual-serving windows from the versioning policy
SUPPORT_FIRST_YEAR  = 4    # per consumer, from an existing platform service
SUPPORT_LATER_YEARS = 1    # per consumer, once integrated
FIX_PER_CHANGE      = 1.5  # per integration, per upstream breaking change

def shared(years, n):
    fixed = HOSTING_PER_YEAR + ONCALL_PER_YEAR + VERSIONING_PER_YEAR
    total = SERVER_BUILD + SERVER_AUTHZ + CLIENT_ONBOARD * n
    total += SUPPORT_FIRST_YEAR * n              # integration year
    total += (fixed * years) + SUPPORT_LATER_YEARS * n * (years - 1)
    return total

def duplicated(years, n, changes_per_year):
    build = FIRST_INTEGRATION + FOLLOW_INTEGRATION * (n - 1)
    upkeep = FIX_PER_CHANGE * n * changes_per_year * years
    return build + upkeep

for c in (1, 6):          # provider changelog: 2 breaking changes in 24 months
    for y in (1, 2, 3):   # the fast case is a provider mid-migration
        print(c, y, round(shared(y, 4), 1), round(duplicated(y, 4, c), 1))

Two of those constants are themselves the result of an arithmetic somebody did, and both of them close, which is what makes the model portable to a company with different bills. Hosting is a monthly invoice turned into days: a hundred and eighty dollars a month is two thousand one hundred and sixty dollars across twelve months, and at a loaded nine hundred dollars a day that is two and two-fifths engineer-days, which is HOSTING_PER_YEAR exactly. On-call is the same move made from the other end: half a day a month, twelve months, six days. Swap in your own invoice and your own loaded rate and both constants recompute without a line of the model changing, which is the property that separates a model from a set of numbers somebody once agreed to.

A third constant carries a comment that looks like it contradicts the prose above the code, and it does not. The comment says followers copy about sixty percent of the work; the prose says a follower lands in about forty percent of the time. Those are the two halves of one fraction — what is copied and what is left to do have to add up to the whole job — and FOLLOW_INTEGRATION at four days against FIRST_INTEGRATION at ten is the forty, which leaves the sixty as the part nobody paid for twice. A reader who reconciles the two has also checked the constant against its own comment, which is the only reason to write the comment in that form.

HorizonShared serverFour direct integrations, 1 upstream change/yrFour direct integrations, 6 changes/yr
Year 176.4 days28 days58 days
Year 292.8 days34 days94 days
Year 3109.2 days40 days130 days

One cell worked in full makes the rest of the table believable, and year one on the shared side is the one to do. Thirty days build the server and ten more buy the token verification and per-consumer scoping the proposer's estimate left out, which is forty before anybody has integrated. Four consumers at two days of onboarding each is eight, and four consumers at four days of first-year support each is sixteen, so the consumer-attributable part is twenty-four and the running total is sixty-four. One year of fixed operating cost — hosting, rotation and versioning added together — puts twelve and two-fifths on top of that, and sixty-four plus twelve and two-fifths is the seventy-six and two-fifths already in the table. Every later row is that same sixty-four with another year of the same fixed cost stacked on it, plus a day per consumer for each year after the integration year.

At the change rate the provider's own changelog supports — two breaking changes in 24 months — the shared server does not break even in three years and is not approaching it, because its recurring cost of about 16 days a year is more than double the six days a year that four duplicated integrations spend absorbing upstream changes. At six changes a year, which is what a provider mid-migration looks like, the server pays back during month 24 and wins clearly after that.

Both quantities in that comparison are rates rather than totals, and they are assembled from different parts of the model. On the shared side the annual recurring cost is the fixed block — hosting, rotation and versioning — plus a day of support for each consumer once its integration year has passed, which at four consumers is twelve and two-fifths plus four. On the duplicated side it is a day and a half per integration per breaking change, which at four integrations and a single change a year is the six. Those are what each option accrues during a year in which nothing else happens at all, and the shared side losing a comparison of rates is what settles the question of the direction the gap moves in: the line that is already higher is also the one climbing faster.

The variable that flips the decision is the upstream change rate rather than the consumer count, and that is not what anyone in the room expects. Consumer count raises both sides at once: each additional consumer adds two days of onboarding and a support tail to the shared side, and one follower integration plus its share of upkeep to the duplicated side. The change rate raises one side only, because duplicated maintenance is the only cost in the model multiplied by both N and c.

Reading that off the model is quicker than reasoning about it. Ask what one more consumer adds to each side: on the shared side, two days of onboarding once, four days of support during its integration year and a day a year afterwards; on the duplicated side, a follower integration at four days and its own share of the per-change upkeep. Both go up, and at a low change rate they go up by amounts close enough that the ordering barely shifts. Then ask what one more breaking change a year adds. On the shared side it adds nothing, because no constant in the model is multiplied by the change rate at all. On the duplicated side it adds a day and a half for every integration, every year, for as long as the horizon runs. A parameter that appears in one side's arithmetic and is absent from the other's is the only kind that can reverse the comparison, and the change rate is the only parameter here that qualifies.

Where the flip actually comes from

Running the model with one parameter moved at a time is more useful than the totals, because it tells you which fact to go and check rather than which answer to prefer.

ParameterBaseServer wins when
Upstream breaking changes per year14 or more, at 4 consumers
Consumers426 or more, at 1 change/year
Fixed operating cost per year12.4 daysunder 4 days — a mature platform where hosting, auth and on-call are marginal
Per-consumer support4 days year one, 1 afteralready low; halving it moves break-even by under two months

"Server wins when" in that table is not the horizon at which the cumulative totals cross, and a reader who tries to reproduce it as one will land nowhere near these figures. It is the point at which the two annual recurring rates cross — which option is cheaper to keep, with the build already paid for and behind you. Set the shared side's rate, the fixed operating cost plus a day per consumer per year, equal to the duplicated side's, a day and a half per consumer per breaking change per year. At one change a year the consumer count that balances them is just under twenty-five. Hold the consumer count at four instead and let the change rate float, and the balance falls at a little under three changes a year. Both thresholds in the table are then quoted one whole unit above the first integer that clears the crossing, which rounds against the server and is the direction a reviewer spending somebody's budget should want them rounded.

What a rate crossing licenses is a claim about upkeep and nothing beyond it. Above the threshold the shared server is the cheaper thing to own from that year forward; it does not follow that it has repaid what it cost to build, and on these constants it has not — a rate that wins by a fraction of a day a year takes a very long time to work off sixty-four days of one-time cost. Quoting a rate crossing as a break-even is the commonest way a model like this gets misused, and the two give different answers because they are different questions: one asks which line is lower, the other asks which area underneath it is smaller.

The third row is the one worth acting on. A company where a new MCP server means a chart, an entry in an existing gateway that already handles token verification and tenant scoping, and inclusion in a rotation that covers thirty services can plausibly get fixed operating cost to three or four days a year, and the whole calculation changes: break-even at 1 change a year falls from 26 consumers to about 8. This is the concrete form of the point that an existing platform is real evidence while the existence of a standard is not. It also means the same proposal is correct at one company and wrong at another with identical consumers, which is uncomfortable and true.

The size of that move is the same equation run again with one term reduced. With fixed operating cost at four days rather than twelve and two-fifths, the shared side's annual rate becomes four plus a day for each consumer, and setting that equal to a day and a half for each consumer balances at eight consumers exactly. The fixed block is the whole of the constant term on the shared side, so the threshold moves in strict proportion to it: the crossing is that constant divided by the margin one extra consumer swings, and dividing the constant by three divides the threshold by three. Which is why the figure a reviewer should send somebody to fetch is the platform's own operating days per service, rather than anybody's forecast of consumers — one of those two numbers sets where the threshold sits and the other is merely measured against it.

The consumer-count row should be read as a warning about the model rather than as a target. Twenty-six is not a number anyone should quote in a meeting, because past roughly ten consumers the assumptions stop holding: support does not stay at one day per consumer per year when the twentieth team arrives with a use case nobody designed for, and duplicated integrations at that scale would have produced a shared library long before anyone proposed a protocol. What the row tells you is the direction and the rough magnitude — at a low upstream change rate, consumer count alone has to get implausibly large before it repays a fixed operating cost, which is why "we will have lots of consumers eventually" is a weak argument and "the provider deprecates an endpoint every quarter" is a strong one.

The sourcing rules matter as much as the structure. Build estimates come from the team that will do the work rather than from the person proposing it, because a proposer's estimate is a bid. Hosting comes from an actual bill for a comparable service. On-call comes from the platform's own record of incidents per service per quarter, which is a number most platform teams have and rarely apply to new proposals. Support load comes from measuring an existing internal service — count a quarter of its Slack mentions and tickets, divide by consumers. Upstream change rate comes from the provider's public changelog, which is the single cheapest number to obtain and the one that decides the model.

Latency belongs in the presentation as a line that is small rather than as a line that is missing. Chapter 1 measured 27 ms of median overhead for a comparable in-process to over-the-wire move, and an agent that makes five tool calls in a turn wears about 135 ms of it. For an interactive assistant that is inside the noise, and saying so with the number is more credible than leaving it out and being asked.

The arithmetic behind that second figure is worth showing, because it is the kind a reader does in their head and gets wrong. Twenty-seven milliseconds is a difference between two medians, in-process against over-the-wire, and a turn that makes five calls wears the difference five times, which is where the hundred and thirty-five comes from. Two conditions ride on it and both belong beside it in the presentation. It is a median, so it says nothing about the turn in which one of those five calls lands in the tail rather than at the middle. And it was measured in one team's staging environment against one database, so it carries across as an order of magnitude rather than as a number — which is all the claim being made needs, since the claim is only that latency is a small line rather than a missing one.

A model is precise about the estimable and silent about the decisive

If the billing capability turns out to hold a credential the agents must not hold, or to be owned by a team that is audited on it, the model stops being the argument. Ownership and security constraints are not costs you trade against, and no consumer count makes them go away. Running the numbers anyway is still worth doing, because the output changes from "should we build it" to "what does the boundary we are required to have actually cost", which is a budget conversation and a staffing one.

There is also a sensitivity rule that keeps these models honest. If the answer flips on a 20% change in an estimate — and in the base case above it flips between one and four upstream changes a year — then the cost case is not strong enough to carry the decision on its own, and the record should say so rather than presenting a total to one decimal place. A model whose conclusion is "this is close, and here is the single fact that would settle it" is more useful than one whose conclusion is a number, because the first sends somebody to the changelog and the second ends the conversation.

The shape the model makes visible is the whole of its value. Fixed costs start on ship day and continue whether or not anything happens; variable costs bill only when the upstream moves. A server's hosting, rotation, versioning and support run every month at four consumers and at zero, and duplicated integrations sit quietly until a provider deprecates something. That asymmetry is why the totals cross where they do, and why an argument about how many teams will eventually use the thing is answering the wrong question.

Everything above assumes the boundary works. The recurring lines cover a normal month, and the months that are not normal are where a boundary charges its real premium, because the seven added failure modes are not evenly distributed across the year. Approving on cost still leaves you owing a demonstration that the boundary survives an incident, and the next chapter is about running that demonstration before the thing ships.

When MCP is the wrong answer — read the current version at https://daviddominguez.dev/learn/en/when-mcp-is-the-wrong-answer/fixed-and-variable-costs-of-a-boundary

Back to top