Skip to content
Stribog

Optionality

All writing

Pricing the Exit: Vendor Lock-In as a Number, Not a Feeling

Vendor lock-in is a liability you can compute: egress bytes times price, proprietary API surface, data gravity, retraining. Here is the ledger.

Stribog13 min read

Everyone agrees lock-in is bad. Nobody can say what leaving costs. So the discussion resolves as a feeling, the feeling loses to a delivery date, and eighteen months later the number nobody computed is the number nobody can pay. The fix is arithmetic: per dependency, on a schedule, with a gate that fails when it goes stale.

Lock-In Is a Liability, Not a Mood

A liability has a value, a currency and a date. Treat every dependency you have not left as one, in four terms.

  1. Egress mass times the published rate. The stored bytes that must physically move, priced in the tier you would land in — not the headline tier, and not zero.
  2. Proprietary API surface. The interfaces you have written against with no second implementation. Not "we use a lot of managed services": a number, from state.
  3. Data gravity. Not the dataset's size but its growth rate — the coefficient enlarging terms one and two every quarter you do nothing.
  4. Human cost. Runbooks rewritten, engineers retrained, weeks of dual-running on two bills and two rotations.

Term one is what everybody argues about and almost never the largest — it dominates only because it is the term with a published price. This is the Optionality pillar made quantitative, not a migration playbook: cloud repatriation is the mechanics of a move already decided. This is the measurement that decides it, run on dependencies you never intend to leave.

One number per dependency, two things that consume it. Note where the regulation attaches: to part of one term, in one jurisdiction.

Term One: Multiply the Real Bytes by the Real Rate

Two numbers, both of which teams guess. The bytes: ninety days from your billing export, not an estimate of "how big is the database." The rate: read off the pricing page today, because it moved since somebody last quoted it. As published on 2026-08-04: Azure internet egress from North America or Europe over the Microsoft Premium Global Network is free for the first 100 GB per month, then $0.087 per GB for the next 10 TB, tapering to $0.05 in the next-350-TB band. Google Cloud's Premium Tier transfer out to North America is free for the first 1 GiB per month, then $0.12 per GiB to 1,024 GiB. AWS gives every customer 100 GB free per month, aggregated across all services and regions except China and GovCloud.

bash
#!/usr/bin/env bash
# Usage: DATASET_GB=42000 ./exit-egress.sh egress-90d.csv
# CSV is one row per day from your billing export: YYYY-MM-DD,egressed_gb
set -euo pipefail

: "${DATASET_GB:?the stored mass you must actually move, in GB}"
MBPS="${SUSTAINED_MBPS:-500}"   # measured throughput, not the link rate
FREE="${FREE_GB_MONTH:-100}"
# Azure internet egress, NA/Europe, Premium Global Network, read 2026-08-04,
# as "cumulative_billed_gb:usd_per_gb".
TIERS="${TIERS:-10240:0.087,51200:0.083,153600:0.07,512000:0.05}"

awk -F, -v move="$DATASET_GB" -v free="$FREE" -v tiers="$TIERS" -v mbps="$MBPS" '
  $2 + 0 == $2 { gb += $2; d += 1 }
  END {
    if (d == 0) { print "no numeric rows in export" >"/dev/stderr"; exit 2 }
    # The allowance is monthly and ordinary traffic spends it first: price the
    # billable slice with the move, less the billable slice without it. Below
    # the allowance both are zero.
    base = gb / d * 30.44
    lo0 = base > free ? base - free : 0
    hi0 = base + move > free ? base + move - free : 0
    n = split(tiers, t, ",")
    for (i = 1; i <= n && hi0 > lo0; i++) {
      split(t[i], kv, ":"); cap = (i == n) ? hi0 : kv[1] + 0
      lo = lo0 > prev ? lo0 : prev; hi = hi0 < cap ? hi0 : cap
      if (hi > lo) cost += (hi - lo) * (kv[2] + 0)
      prev = cap; if (prev >= hi0) break
    }
    days = (move * 8000) / (mbps * 86400)
    printf "baseline egress  %d GB/month\nbillable exit GB %d of %d moved\n", base, hi0 - lo0, move
    printf "transfer at list $%.2f\ncopy time %.1f days at %d Mbps\n", cost, days, mbps
    # A bill you can pay is not a migration you can finish.
    if (days > 30) { print "STOP: the copy alone exceeds 30 calendar days."; exit 1 }
    printf "OK: fits 30 calendar days with %.1f days of slack.\n", 30 - days
  }' "${1:?usage: exit-egress.sh <billing-export.csv>}"
Term one from measured reality: a move still inside the monthly free allowance prices at zero; past it, the baseline decides which tier the exit starts in. The top tier extrapolates beyond the last published band — above that you are in a negotiated rate, so treat the figure as a ceiling and get it in writing.

Then find out who pays. "Egress is free when you leave now" is folklore over three different mechanisms, none an automatic zero on the invoice.

  • AWS offers eligible customers free transfer out "when they move all of their data off of AWS or all of their data off of a particular AWS service," via Customer Support. Its announcement post carries a 30 September 2025 update: "Eligible customers will have 90 days to complete their move off of AWS," plus a separate EU Data Act Addendum.
  • Azure waives egress for customers leaving "to switch to another cloud provider or an on-premises data center," but above the standard 100 GB per month the pricing FAQ says to "follow these steps to claim your credit."
  • Google Cloud removed the fees globally on 12 January 2024 for customers migrating "to another cloud provider and/or on premises" (announcement). The exit page gives the mechanics: an Exit Notice, a 30-day Initiation Period, a Migration Period of at least 30 days, then a Completion Notice. Customers "are not permitted to migrate only a portion of a given Google Cloud service."

Term Two: Count the Proprietary Surface in Your State

Term two gets estimated from memory and comes out wrong in both directions. Your infrastructure-as-code already knows: whether you run Terraform or have moved to OpenTofu, the JSON state representation lists every managed resource with its type, and one pass turns a vibe into a metric.

bash
#!/usr/bin/env bash
# Term two, from live state. Managed and not on the list means locked in.
set -euo pipefail

# Portable means the resource TYPE has a second implementation you could run,
# not that the data format is open. No other provider implements
# aws_rds_cluster, so managed relational services stay LOCKED here even though
# the engine is open-source SQL.
PORTABLE='["aws_s3_bucket","aws_s3_object",
           "kubernetes_namespace","kubernetes_deployment","helm_release"]'

tofu show -json >state.json

jq -r --argjson portable "$PORTABLE" '
  # State nests modules arbitrarily deep; a top-level pass undercounts.
  def all_modules: recurse(.child_modules[]?);

  [ .values.root_module | all_modules | .resources[]? | select(.mode == "managed") ]
  | group_by(.type)
  | map({ type: .[0].type, n: length,
          verdict: (if .[0].type | IN($portable[]) then "portable" else "LOCKED" end) })
  | sort_by(-.n)
  | (map(select(.verdict == "LOCKED") | .n) | add // 0) as $locked
  | (map(.n) | add) as $total
  | (.[] | "\(.n)\t\(.verdict)\t\(.type)"),
    "",
    "term two: \($locked) of \($total) managed resources have no second implementation"
' state.json
Validated against a real state document: modules nest arbitrarily deep, so a top-level pass silently undercounts. The portable list is yours to maintain — honestly.

Resource counts are a proxy — five buckets are one dependency, one queue can be thirty call sites — so use the count to rank, then read the code at the top. Be precise about the two classes teams grade too generously. A managed relational service stays LOCKED even though the engine is open-source SQL: the wire protocol travels; the resource type, failover behaviour, backup format and IAM binding do not — and databases usually dominate real exit cost. Managed Kubernetes only half moves: the workload API is conformant and your Deployments really do move, but the control-plane lifecycle, IAM integration and node-group model are proprietary. "We run Kubernetes, so we are portable" is true of the manifests and false of the platform.

Term Three: Data Gravity Is a Rate, Not a Mass

Dave McCrory named the effect in a post dated 7 December 2010: "Consider Data as if it were a Planet or other object with sufficient mass. As Data accumulates (builds mass) there is a greater likelihood that additional Services and Applications will be attracted to this data." That is a conceptual framing, not a formula — no unit was standardised, so any coefficient is somebody's modelling convention.

Ours is deliberately crude, because a crude number that gets re-measured beats an elegant one that never does: carry data_growth_gb_per_month per dependency and project term one forward five years at that rate. It gives a ranking, not a forecast, and answers one question — which two or three datasets dominate this estate's exit position in 2031?

The answer is rarely the biggest dataset. A 40 TB archive growing at 1% a month is nearly inert; a 4 TB event store growing at 15% passes it inside three years and drags every consumer with it, which is why the analytics tier and object storage decide most exit positions. Gravity is why exit cost compounds: mass attracts services, the services write against the proprietary API, and term two grows because term three did.

Where Abstraction Pays, and Where It Is Just a Tax

The measurement produces a decision, per boundary rather than per estate. One test settles most: is there a second implementation you could actually run in production, today, with the team you have? Not "does a competing product exist" — could you cut over, and has anyone tried?

Where the answer is yes — the S3 HTTP API, OIDC and OAuth2, the OCI registry and image format — the abstraction is nearly free: it is the interface you were writing anyway, and somebody outside the vendor verifies conformance to it. The CNCF's conformance programme is the model: "Eligible vendors are invited to submit conformance testing results for review and certification by the CNCF." A portability claim verified by somebody other than the vendor is worth something; a vendor's assurance that its API is "standard" is not.

Where the answer is no — managed queue semantics, proprietary IAM policy languages, serverless event contracts, vendor model endpoints — a portability layer is a lowest-common-denominator of your own making, paid for every sprint, surrendering the feature you bought the service for. Do not abstract it: use the service properly, price the exit, re-price quarterly.

The third zone is the expensive one: optionality on a slide, a second platform on the bill.

Multi-cloud active/active adopted for portability — rather than for latency or a regulatory requirement — buys an option nobody exercises, at the price of two control planes, two identity models, two bills, and every service built to the intersection of what both providers do. An exit never rehearsed is a hypothesis. A measured dependency you chose is optionality; a portability layer nobody has exercised is neither.

Four Ways an Exit-Cost Model Lies to You

  1. List-price fiction, in both directions. A committed-spend discount means list overstates today's bill and understates the exit, because the discount dies with the commitment you are about to break. Model the exit at list and the status quo at your rate; the gap is the switching cost your contract created.
  2. The one-shot migration fallacy. Real migrations copy, backfill the delta, cut over, find a consumer nobody mapped, and copy again. Budget the bytes at least twice.
  3. Ignoring dual-running. Between first write to the new system and last read from the old you pay for both — months, not weekends, frequently the largest line. OpenCost showback makes that number defensible.
  4. Modelling a voluntary exit. The exits that hurt are involuntary — a licence change, an acquisition, a region withdrawal — each on somebody else's deadline. Run the model twice: four quarters, then ninety days. The second number should govern the architecture.

Make It a Gate, or It Will Rot

A spreadsheet of exit costs stays accurate for about six weeks. The version that survives is the one the pipeline enforces: a record per dependency, versioned beside the code that created it, and a policy that fails the build when a new managed service arrives without one or a record goes stale.

yaml
exit_cost_ledger:
  records:
    - dependency_key: managed-queue-primary
      service: "Azure Service Bus queue + topic fan-out"
      resource_types: [azurerm_servicebus_queue, azurerm_servicebus_topic]
      exit_to: "NATS JetStream on clusters we already run"

      # Term 1 — stored mass at the published list tier, dated.
      egress_gb: 900
      egress_rate_usd_per_gb: 0.087
      egress_rate_source: "azure bandwidth pricing, NA/EU premium, read 2026-08-04"

      proprietary_api_count: 14        # term 2, from state
      data_growth_gb_per_month: 40     # term 3, the rate that matters
      human_cost_days: 35              # term 4, runbooks and retraining
      dual_running_months: 2

      # What the contract promises, in days.
      notice_period_days: 60
      transitional_period_days: 30
      jurisdiction: "EU — Regulation (EU) 2023/2854 in scope"
      last_repriced: "2026-07-14"
ledger/exit-cost.yaml — one record per dependency you have not left yet.
rego
package main

import rego.v1

# Types with no second implementation you could run. Extend as you adopt
# services — maintaining this set IS the work.
managed_service_types := {
	"aws_sqs_queue", "aws_sns_topic", "aws_kinesis_stream",
	"aws_dynamodb_table", "aws_eks_cluster",
	"google_pubsub_topic", "azurerm_servicebus_queue",
}

day_ns := (24 * 3600) * 1000000000

records := data.exit_cost_ledger.records

created contains rc if {
	some rc in input.resource_changes
	"create" in rc.change.actions
	rc.type in managed_service_types
}

covered(t) if {
	some r in records
	t in r.resource_types
}

# 1 — A new managed-service dependency arrives with no price on it.
deny contains msg if {
	some rc in created
	not covered(rc.type)
	msg := sprintf("%s creates %s with no exit-cost record.", [rc.address, rc.type])
}

# 2 — The ledger rots by doing nothing, so staleness fails the build too.
deny contains msg if {
	some r in records
	age := time.now_ns() - time.parse_ns("2006-01-02", r.last_repriced)
	age > (90 * day_ns)
	msg := sprintf(
		"exit-cost record %q last repriced %s, %v days ago. Re-price it.",
		[r.dependency_key, r.last_repriced, floor(age / day_ns)],
	)
}
policy/exit_cost.rego — verified against OPA 1.19.0 via `conftest test --policy policy --data ledger plan.json`, where plan.json is `tofu show -json` over a plan file.

The second rule is the one that matters. The first is easy to satisfy once and forget; the second means the ledger cannot be quietly abandoned, because ninety days after the last honest re-pricing every pipeline turns red until somebody re-reads the pricing page. That is the mechanism — not a policy that lock-in is bad, but a build that fails when nobody has priced it lately. Start with identity and the event backbone: small enough to price precisely, coupled deeply enough to hurt.

The Data Act Put a Clock on Someone Else's Half of the Bill

For a customer switching in scope of Regulation (EU) 2023/2854, several of these numbers stopped being negotiable. It "shall apply from 12 September 2025," and repays reading in the operative text rather than the commentary.

  • Notice is capped at "a maximum notice period ... which shall not exceed two months."
  • Transition is capped, by default — a switch, or porting "all exportable data and digital assets to an on-premises ICT infrastructure," must complete within "the mandatory maximum transitional period of 30 calendar days."
  • But 30 days is not a wall. Where it "is technically unfeasible," the provider must notify "within 14 working days," justify it, and "indicate an alternative transitional period, which shall not exceed seven months." The customer separately holds "the right to extend the transitional period once."
  • Charges taper, then stop. To 12 January 2027 providers "may impose reduced switching charges," not exceeding "the costs ... directly linked to the switching process." From that date they "shall not impose any switching charges."
  • Format is specified. Where no common interoperability specification exists for the service type, the provider "shall ... export all exportable data in a structured, commonly used and machine-readable format."
  • Repatriation is protected, not a loophole. Article 23 covers switching to another provider "or to on-premises ICT infrastructure," with providers required to "remove pre-commercial, commercial, technical, contractual and organisational obstacles."

So the Act is worth one thing in the model: it constrains part of term one for in-scope switches and hands you dates to test the arithmetic against — which is why the egress script prints days as well as dollars. Where the answer also turns on where the data sits, the sovereign cloud build-vs-buy framework is the companion decision.

The Long Game: Price the Exit While It Is Still Small

Exit cost is monotonic under neglect: every quarter you do not measure it, gravity adds mass, the mass attracts services, and the services write against surfaces with no second implementation. The cheapest moment to price a dependency is the sprint you adopt it, when the number is small enough that nobody argues.

So set a cadence and a threshold. Quarterly: re-read the published rates, re-measure the bytes, update last_repriced. Set the threshold in advance as a number the business would authorise, and when a dependency crosses it treat that as a design input, not a risk-register note: the next service is not built on it, or the coupling gets an interface, or the migration goes on a roadmap while it is still a project rather than a crisis.

None of this argues for leaving. Most dependencies on a well-run ledger never will be, and that is the correct outcome — a priced dependency is a decision rather than a drift. Sovereignty is not the absence of vendors. It is knowing, in currency and in days, what each costs to leave, and having chosen anyway.

§FAQ/Common questions

Frequently asked

Is vendor lock-in avoidable?

No, and chasing zero lock-in is its own expensive mistake. Every dependency you take — including open-source ones you self-host — creates coupling you would have to unwind. The goal is not zero, it is measured: a number in currency and in days for each dependency, re-priced on a schedule, so staying is a decision rather than an accident. Teams pursuing zero lock-in usually end up building a portability layer over interfaces with no second implementation, paying a lowest-common-denominator tax every sprint for an option nobody exercises. An honest, priced dependency on the right managed service beats that.

How do I calculate the cost of vendor lock-in?

Four terms per dependency. One: the stored bytes that must move, priced at the published egress tier you would actually land in, remembering that your ordinary monthly egress has already consumed the free allowance and started you up the ladder. Two: the count of proprietary interfaces you have written against, taken from your Terraform or OpenTofu state rather than from memory. Three: the growth rate of the underlying data, projected forward, because the exit cost compounds. Four: the human cost — runbooks, retraining and dual-running weeks, which is usually the largest term and always the one people omit. Record all four in a versioned file per dependency and re-price them quarterly.

Does the EU Data Act make cloud egress free?

No. Two limits matter. First, timing: from 11 January 2024 to 12 January 2027 providers may impose reduced switching charges that must not exceed the costs directly linked to the switching process, and only from 12 January 2027 are switching charges prohibited outright. Second, scope: Article 29 governs charges for the switching process, not the operational egress you pay every month for ordinary traffic, and the Regulation reaches customers in the EU rather than everywhere. Outside a formal in-scope switch, or outside the EU, list price is what you budget against. Model the bill at list and treat any provider waiver as a discount you may win.

Is the hyperscalers' free exit egress automatic?

No. All three are processes you enter, not a zero that appears on the invoice, and the mechanisms differ. AWS scopes its waiver to moving all data off AWS or off a particular service, routes it through Customer Support, and its blog's 30 September 2025 update gives eligible customers 90 days to complete the move. Azure waives egress for customers leaving, but above the standard 100 GB per month you follow steps to claim a credit after the fact. Google is not discretionary — all customers who follow the steps are eligible and the credit adjustment is applied automatically to the final invoice — but the programme still runs through an Exit Notice, a 30-day Initiation Period, a Migration Period of at least 30 days and a Completion Notice, and does not permit migrating only a portion of a service.

Does running Kubernetes protect me from cloud vendor lock-in?

Partly, and the boundary is sharper than most teams assume. The workload API is genuinely portable — Certified Kubernetes conformance is verified by the CNCF on submitted test results, so the claim is checked by somebody other than the vendor making it. What does not move is everything around it: the control-plane lifecycle, the IAM integration, the load-balancer and ingress controllers, the node-group model, the CSI drivers and the managed add-ons. A managed-Kubernetes dependency lands in the ledger with a small egress term and a substantial proprietary-API and human-cost term. Count it from state like anything else rather than exempting it because the product name contains the word Kubernetes.

vendor lock-incloud vendor lock-in avoidance architectureexit cost modelling data gravity egresskubernetes vendor lock-in managed serviceproprietary api abstraction boundary designmulti-cloud portability reality check

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.