Skip to content
Stribog

FinOps

All writing

Repatriation Cost: A Five-Year TCO Model You Can Defend

Repatriation TCO past the hardware quote: useful life and depreciation, the refresh a three-year model hides, loaded staffing, and where cloud still wins.

Stribog13 min read

Most repatriation cases are rejected for accounting reasons, not engineering ones. The migration sequence is credible, the audit real, the team can run the metal. What fails is the page finance reads: a cloud invoice, a hardware quote, and no statement of the useful life, the refresh date, or the headcount it all rests on.

Why the hardware quote loses the room

A capital approval review asks four questions a two-column comparison cannot answer. Over what useful life is the asset depreciated, on what basis? When does cash leave again for replacement hardware, and is that the same number? What does the headcount cost with benefits loaded on? What discount rate applies, when capex lands in year zero and savings arrive later?

None of those are hostile. They are the standard apparatus for any capital project, and a model that cannot answer them is rejected as incomplete, not on the merits. The engineering side is covered in Stribog's own repatriation playbook and its worked thirty-six month cash comparison — that is the cash-flow example, this is the accounting underneath it.

The gap is acknowledged. Stribog's published repatriation TCO calculator calls itself "a directional three-year cost model", and names its limits: "Capex treatment, leasing, and depreciation schedules materially change the cash-flow picture. The model uses a simple one-time migration cost." This is the part it leaves out.

Dashed means an input nobody quoted you and no filing will settle. The three panels on the right are the quantities a single 'refresh assumption' collapses into one.

Useful life is three different numbers, and you can source each one

Almost every on-prem model contains one figure called the refresh assumption, silently doing three incompatible jobs. Separating them is the highest-leverage edit available: each has a citable source, and reviewers accept citations where they reject assertions.

Book useful life is what you depreciate against — an election you make and defend. Amazon's FY2025 Form 10-K gives servers and networking equipment an estimated useful life of "Five to six years": it lengthened server lives from five to six effective January 1, 2024, then shortened a subset from six to five effective January 1, 2025, attributing that to "the increased pace of technology development, particularly in the area of artificial intelligence and machine learning". It does not call five years an AI-fleet default or six general-purpose: a subset changed, and why. Alphabet's FY2025 10-K is a cleaner anchor — "We depreciate servers and network equipment generally over a period of six years."

Tax recovery period is a different question with a different authority. IRS Publication 946's recovery-period table under the Alternative Depreciation System assigns "Computers and peripheral equipment" five years — a US federal tax figure, not a book election, landing in the same band by coincidence. It covers one class; network fabric, racks, PDUs and plant fall elsewhere, and outside the US it settles nothing.

Observed refresh is when cash actually leaves. The Uptime Institute's Global Data Center Survey 2026 reports server life cycles rising "from around 36 to 44 months" between 2015 and 2023, then reversing, with reported lifetime falling "to around 41 months". That is a self-reported survey mean across facility types, not a figure for Kubernetes nodes: a moderately loaded node plausibly runs longer, a GPU node shorter. Use it as an anchor, with the refresh year an explicit input rather than a consequence of the book life.

The five-year shape: refresh, spares, residual, and the power line

Three lines exist in a five-year model that a thirty-six month model cannot show. The first is the refresh: if the cadence sits near forty-one months and the window closes at thirty-six, the largest cash outflow after year zero falls outside by construction — the horizon chosen before the cadence.

The second is the spares pool, usually substituted by a percentage-of-capex maintenance rule — a number with no derivation behind it. A published failure rate is better: Backblaze's Q1 2026 Drive Stats report gives a lifetime annualised failure rate of 1.39 percent, and 1.24 percent for the quarter across 341,263 drives. Name the extrapolation: hard drives in one operator's storage-cloud duty cycle, not servers in yours. It can be argued with rather than believed.

The third is residual value, where the honest move is to assert nothing. No public recovery fraction exists for a mixed server fleet; realisation swings with generation, memory configuration and timing. Uptime notes the direction: shorter cycles mean equipment loses value faster. Make it a named input the reviewer sets.

Colocation pricing deserves the same treatment: Equinix and most wholesale operators publish no rate card, and every secondary aggregate is stale or regional. Power is misread more often still. The Uptime Institute's 2026 survey puts the industry average PUE at 1.52, "a figure that falls right in line with a seven-year trend of relative stasis", with respondents' largest facilities averaging 1.45 globally and 23 percent below 1.3. That explains why a self-operated room and a colocation quote price power differently; it does not compute your bill, since contracts commonly bill committed kW or metered IT load with a pass-through facility factor. Attribute it to the 2026 survey by name — the 2025 edition reported 1.54.

Staffing: the largest line, and the one that gets absorbed

Staffing is usually the largest recurring line and the one most often set to zero, on the reasoning that the platform team already exists. That is a capacity claim disguised as a cost claim: if the team absorbs provisioning, RMA, capacity planning and a hardware-aware rota without extra headcount, something else stops. Price what stops, or price the headcount.

Published data gets you there in two steps. The US Bureau of Labor Statistics reports a median annual wage of $99,130 for network and computer systems administrators in May 2025. Do not use it: that code under-represents a senior platform engineer also fluent in bare-metal provisioning. The same profile reports the highest-paid 10 percent "earned more than $155,050" — closer, and likely still low in a high-cost metro.

Step two is the load. The BLS Employer Costs for Employee Compensation release for June 2026 reports wages and salaries of $32.82 against $46.89 per hour for private industry workers, "accounted for 70.0 percent of employer costs". One divided by 0.700 gives roughly 1.43x base salary — a derivation, not a BLS-stated multiplier, scoped to all private industry rather than professional or IT occupations. Recruiting, equipment and overhead sit on top.

Then size the rota, not the headcount. A rotation that does not burn people needs enough engineers that any one can be unavailable; below roughly three, it is a person with a phone.

Price the cloud side honestly or lose the argument

The fastest way to lose a repatriation review is to compare against on-demand list prices, which nobody at scale pays. AWS states EC2 Instance Savings Plans offer "savings up to 72%" for commitment to individual instance families in a Region, while the flexible Compute Savings Plans — applying "regardless of instance family, size, AZ, Region, OS or tenancy" and to Fargate and Lambda — "reduce your costs by up to 66%". Spot is advertised at up to 90 percent off On-Demand for fault-tolerant workloads. Vendor ceilings, not achieved rates — your baseline must be what you pay.

Which means invoices, and in Cost Explorer the amortised metrics specifically. Under the unblended view, Savings Plan covered usage is recorded at on-demand rates alongside a separate commitment charge, mis-pricing the exact baseline this argument depends on. Grouping by purchase type and record type makes the split visible.

bash
# Committed cloud baseline, one linked account, last full year.
aws ce get-cost-and-usage \
  --time-period Start=2025-09-01,End=2026-09-01 \
  --granularity MONTHLY \
  --metrics AmortizedCost NetAmortizedCost \
  --group-by Type=DIMENSION,Key=PURCHASE_TYPE \
             Type=DIMENSION,Key=RECORD_TYPE \
  --filter '{"Dimensions":{"Key":"LINKED_ACCOUNT","Values":["<YOUR_ACCOUNT_ID>"]}}'
Excerpt — substitute your own account ID and dates. AmortizedCost spreads the Savings Plan commitment across covered usage; UnblendedCost does not, which is the trap. There is no --account flag on this command; scope with a LINKED_ACCOUNT filter.

Two corrections keep it honest. Size the column for the same peak capacity the asset register provisions, so both sides describe one workload; and carry a growth rate, because the on-prem case must beat where the bill is going. Where per-namespace attribution is missing, OpenCost showback beats a spreadsheet reconstruction and survives the migration.

The model, as code you can run

Inputs first, in a file you hand to finance with the output. Every assumption named, the three lines with no public source nullable — the model refuses to run on defaults nobody agreed to.

yaml
# Three inputs have no public source: colocation space and power, transit, and
# the residual fraction. Null any of them and the model refuses to run.
horizon_years: 5
discount_rate: 0.09 # your hurdle rate, not a market constant
tax_rate: 0.25 # your effective rate; it drives the depreciation shield

cloud:
  # Amortised committed rates from Cost Explorer, sized for the SAME peak
  # capacity the register below provisions. Not list price.
  annual_at_full_capacity: 2200000
  growth_rate: 0.10

onprem:
  assets: # name, capex, elected BOOK life, when cash leaves again, resale
    - { name: compute_nodes, capex: 640000, useful_life_years: 5, refresh_year: 4, residual_fraction: 0.10 }
    - { name: network_fabric, capex: 180000, useful_life_years: 7, refresh_year: 7, residual_fraction: 0.05 }
    - { name: storage, capex: 240000, useful_life_years: 5, refresh_year: 6, residual_fraction: 0.08 }
  spares:
    population: 480 # field-replaceable units in the fleet
    annual_failure_rate: 0.0139 # Backblaze lifetime AFR: drives, one operator
    coverage_multiple: 2.0 # shelf units per expected annual failure
    unit_cost: 620
  colocation:
    annual_space_and_power: 216000 # ← YOUR quote. No public rate card exists.
    annual_transit: 84000 # ← YOUR quote.
  staffing:
    headcount: 3.0
    base_salary: 155050 # BLS 15-1244 top decile, May 2025 — see the text
    loaded_multiplier: 1.43 # 1 / 0.700, ECEC June 2026, all private industry
  annual_tooling_and_support: 90000
  migration_cost: 400000 # year 0, expensed
five-year-tco.yaml — the artifact that goes in the appendix.

The model is stdlib plus PyYAML. Depreciation is non-cash, reaching cash only through the tax shield — which is why the book life everyone argues about moves the P&L more than the decision.

python
#!/usr/bin/env python3
"""Five-year repatriation model: after-tax cash both sides, then two grids.
Stdlib + PyYAML.  Usage: python3 five_year_tco.py five-year-tco.yaml"""
import sys, yaml


def load(path):
    cfg = yaml.safe_load(open(path))
    o = cfg["onprem"]
    blank = [k for k, v in o["colocation"].items() if v is None]
    blank += [a["name"] for a in o["assets"] if a["residual_fraction"] is None]
    if blank:
        sys.exit("fail closed — fill from your own quotes: " + ", ".join(blank))
    return cfg


def onprem(cfg, life=None, refresh=None):
    # Depreciation is non-cash: it reaches this column only as a tax shield.
    o, n, t = cfg["onprem"], cfg["horizon_years"], cfg["tax_rate"]
    cash = [0.0] * (n + 1)
    for a in o["assets"]:
        yrs, cap = life or a["useful_life_years"], a["capex"]
        ry = refresh if refresh and a["name"] == "compute_nodes" else a["refresh_year"]
        buys = [0] + ([ry] if ry <= n else [])
        for i, buy in enumerate(buys):
            cash[buy] += cap
            retired = i == 0 and len(buys) == 2   # replaced inside the horizon
            sold = ry if retired else n
            for y in range(buy + 1, min(buy + yrs, sold) + 1):
                cash[y] -= t * cap / yrs          # shield stops at disposal
            book = cap * max(0.0, 1 - (sold - buy) / yrs)
            gross = cap * a["residual_fraction"]
            cash[sold] -= gross - t * (gross - book) if retired else book
    s, f = o["spares"], o["staffing"]
    opex = (sum(o["colocation"].values()) + o["annual_tooling_and_support"]
            + s["population"] * s["annual_failure_rate"] * s["coverage_multiple"] * s["unit_cost"]
            + f["headcount"] * f["base_salary"] * f["loaded_multiplier"])
    for y in range(1, n + 1):
        cash[y] += opex * (1 - t)
    cash[0] += o["migration_cost"] * (1 - t)
    return cash


def cloud(cfg, util=1.0):
    c, n, t = cfg["cloud"], cfg["horizon_years"], cfg["tax_rate"]
    base = c["annual_at_full_capacity"] * util
    return [0.0] + [base * (1 + c["growth_rate"]) ** (y - 1) * (1 - t) for y in range(1, n + 1)]


def npv(flows, r):
    return sum(f / (1 + r) ** y for y, f in enumerate(flows))


def main(path):
    cfg = load(path)
    r, n = cfg["discount_rate"], cfg["horizon_years"]
    on, cl = onprem(cfg), cloud(cfg)
    print(f"{n}y horizon | discount {r:.0%} | tax {cfg['tax_rate']:.0%} | after-tax cash")
    print("year        on-prem          cloud")
    for y in range(n + 1):
        print(f"{y:>4}  {on[y]:>13,.0f}  {cl[y]:>13,.0f}")
    print(f"\nNPV on-prem  {npv(on, r):>13,.0f}\nNPV cloud    {npv(cl, r):>13,.0f}")
    print(f"advantage    {npv(cl, r) - npv(on, r):>13,.0f}   (positive = on-prem cheaper)")
    utils = [0.50, 0.65, 0.80, 0.90, 1.00]
    print("\nNPV advantage, $k — compute refresh year x sustained utilisation")
    print("refresh " + "".join(f"{u:>9.0%}" for u in utils))
    for ry in (3, 4, n + 1):
        flows = onprem(cfg, refresh=ry)
        print(f"{(f'{ry}y' if ry <= n else 'none'):>7} " + "".join(
            f"{(npv(cloud(cfg, u), r) - npv(flows, r)) / 1000:>9,.0f}" for u in utils))
    print("'none' = refresh outside the window — what a 36-month model assumes by construction")
    print("\nsame comparison, varying only the elected book life (utilisation 100%)")
    for yrs in (4, 5, 6, 7):
        print(f"{yrs:>4}y book life   advantage {(npv(cl, r) - npv(onprem(cfg, life=yrs), r)) / 1000:>8,.0f}k")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "five-year-tco.yaml")
five_year_tco.py — run it against the YAML above. Both disposal paths are modelled: an asset replaced inside the horizon is sold at a taxed gain or loss against its remaining book value; one still held at the horizon is credited at net book value.
text
5y horizon | discount 9% | tax 25% | after-tax cash
year        on-prem          cloud
   0      1,360,000              0
   1        747,150      1,650,000
   2        747,150      1,815,000
   3        747,150      1,996,500
   4      1,307,150      2,196,150
   5        183,721      2,415,765

NPV on-prem      4,296,680
NPV cloud        7,708,964
advantage        3,412,284   (positive = on-prem cheaper)

NPV advantage, $k — compute refresh year x sustained utilisation
refresh       50%      65%      80%      90%     100%
     3y      -536      620    1,776    2,547    3,318
     4y      -442      714    1,870    2,641    3,412
   none      -378      778    1,934    2,705    3,476
'none' = refresh outside the window — what a 36-month model assumes by construction

same comparison, varying only the elected book life (utilisation 100%)
   4y book life   advantage    3,380k
   5y book life   advantage    3,389k
   6y book life   advantage    3,429k
   7y book life   advantage    3,458k
Actual output for the inputs above. The single NPV at the top is the least useful number on the page; the two grids below it are the deliverable.

Read the grids against each other and the ranking is the finding. Utilisation moves the answer by $3.85M and is the only axis where the sign changes — here between 50 and 65 percent. The two accounting arguments are rounding errors beside it: book life across four to seven years moves $78k, and pushing the refresh outside the window, as a thirty-six month horizon does, moves $64k. The numbers argued over hardest matter least.

Where the answer genuinely stays cloud

A model that cannot return "stay" is not a model. The four axes below are read off the surface above — the inputs the verdict moves on, not a typology observed anywhere.

Every edge condition is read off the model's own sensitivity surface. The honest output is a region, not a number.
  • Sustained utilisation. On-prem cost is fixed once the purchase order is signed; cloud cost follows consumption. Below the crossover, elasticity is worth more than ownership. Autoscaling narrows the gap — KEDA with the descheduler scales to zero on bare metal — but cannot make fixed capacity elastic.
  • Growth uncertainty. A five-year register needs a five-year capacity guess. Where the demand curve is unknown, elasticity is the product being bought and the premium is rational.
  • Absolute scale. Staffing is close to a step function: below it, fixed headcount swamps any hardware saving; above it, one rota covers far more capacity. This axis ends most conversations once staffing is priced.
  • Staffing capacity. Not cost — availability. A rota you cannot fill is a saving borrowed from people who never agreed to lend it.

A repatriation stopped before it starts, on a model the team built itself, is the cheapest outcome here — and what makes it credible next time.

Keep it reversible: the model as an exit-ramp gate

The model's second job starts after the decision. Re-run it annually against actuals — real colocation invoices, real headcount, the real refresh date — and it becomes a standing gate rather than a one-time argument. Inputs drift: utilisation rises as workloads consolidate, discounts are renegotiated, a GPU fleet pulls the refresh forward.

Keep the YAML in version control beside the infrastructure it describes, so a changed assumption is a diff with an author and a date. Run it in CI and let it return either answer — the gate is worthless if only one verdict is acceptable. The logic runs the other way too: pricing the exit as a number rather than a feeling makes the option real. Reversibility is structural: an estate built from declarative sources, with CI compute that runs either side of the line, can move back. One assembled by hand cannot.

The long game: a model you re-run, not a slide you won with

The durable output is not the saving. It is a cost model the team owns, in a file they can diff, with every assumption traceable to a filing, a government release or a quote with a name on it. It prices the next capacity expansion and outlasts whoever argued for it. It also forces a literacy no vendor calculator teaches: separating book life from tax recovery from observed refresh is how a team learns its own economics.

So build the five-year model before the business case, not after the objection. State the useful life and cite it. Make the refresh its own input. Load staffing from published compensation data and leave it visible. Price the cloud side at what you pay. Then publish the surface rather than the number — and be genuinely willing for it to say stay.

§FAQ/Common questions

Frequently asked

What useful life should a repatriation model use for servers?

Elect one and cite it, rather than inheriting a default. Two public anchors bracket the usual band: Amazon's FY2025 Form 10-K books servers and networking equipment at "Five to six years", having lengthened server lives from five to six effective January 1, 2024 and shortened a subset from six to five effective January 1, 2025, which it attributes to "the increased pace of technology development, particularly in the area of artificial intelligence and machine learning". Alphabet's FY2025 10-K states it depreciates servers and network equipment "generally over a period of six years". Both are book elections for hyperscale fleets, not a recommendation for a twenty-node footprint.

Is the IRS five-year figure the same as the useful life I depreciate against?

No, and conflating them is the most common error in these models. IRS Publication 946's Alternative Depreciation System table assigns "Computers and peripheral equipment" a five-year recovery period. That is a US federal tax recovery period, a separate question from the book useful life an operator elects for financial reporting. It is also narrower than a typical asset register: network fabric, racks, PDUs and mechanical plant sit in other classes with other periods.

Why model five years instead of the usual thirty-six months?

Because the refresh usually falls in between. The Uptime Institute's Global Data Center Survey 2026 reports operator-reported server lifetime rising from around 36 to 44 months between 2015 and 2023 and then falling back to around 41 months. A thirty-six month horizon therefore excludes the largest cash outflow after year zero by construction. In the worked model here, pushing the refresh outside the window is worth roughly $64k of NPV, and hides a $560k cash spike in year four outright.

How do you cost platform staffing without guessing?

Start from published wage data, load it, then say what the figures do not cover. The US Bureau of Labor Statistics reports a May 2025 median annual wage of $99,130 for network and computer systems administrators and notes the highest 10 percent "earned more than $155,050"; the top-decile figure is the closer fit for a senior Kubernetes platform engineer with bare-metal experience. The BLS Employer Costs for Employee Compensation release for June 2026 reports wages at 70.0 percent of total employer cost for private industry workers, which yields a roughly 1.43x loaded multiplier — a derivation from that all-private-industry figure, not a BLS-stated multiplier, and before recruiting, equipment or overhead.

What should the cloud side of the comparison be priced at?

Amortised committed rates from your own invoices, sized for the same peak capacity the on-prem asset register provisions, with a growth rate applied. AWS advertises EC2 Instance Savings Plans at "savings up to 72%" and the more flexible Compute Savings Plans at "up to 66%", with Spot at up to 90 percent off On-Demand for fault-tolerant workloads — vendor ceilings, not achieved rates. In Cost Explorer, use AmortizedCost or NetAmortizedCost: under the unblended view, Savings Plan covered usage appears at on-demand rates alongside a separate commitment charge.

repatriation coston premise kubernetes tcocloud vs on premise cost comparison kubernetesserver useful life depreciation scheduledata centre colocation cost model computekubernetes cost per workload bare metal

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.