Skip to content

10 min read

The contract ends in a month; the dependency does not

On this page

The export took forty minutes. Four point two million chunks, 1,536-dimensional vectors and their metadata, 26 GB of newline-delimited JSON in an S3 bucket, produced by a documented endpoint exactly as the vendor's migration guide promised. The team had budgeted two weeks to move off the managed vector database and onto its replacement, and after the first morning that looked generous.

The move took eleven weeks and a further six weeks of paying both vendors while traffic shifted. Nothing went wrong in a dramatic way, no data was lost, and the vendor was helpful throughout. What consumed the time was everything the export did not contain, which was every decision the team had made in eighteen months of using the thing.

Two different questions that the word "locked in" runs together

Lock-in is a question about permission and possibility: are you legally, contractually or technically prevented from leaving? An annual contract with a termination penalty is lock-in. A proprietary format with no export is lock-in. A licence that forbids running a competitor's client against your data is lock-in.

Switching cost is a question about magnitude: given that you are permitted to leave, what does completing the move actually cost in engineering weeks, dual-running spend — both vendors on the invoice for however long it takes to shift traffic across — risk to quality, and elapsed calendar time? These are independent. A component can have zero lock-in and enormous switching cost, which is the normal case for anything that holds state or shapes how you write code against it. The team's answer to the CTO — "it does not lock us in, it is only an API and we could swap it later" — is a correct statement about lock-in offered as an answer to a question about switching cost.

The confusion is easy to make because contract term is visible and switching cost is not. Nobody has to estimate a notice period; it is written on page one. The eleven weeks is a number somebody has to sit down and work out, and there is never an obvious moment to do it, because at signing everyone is optimistic and at leaving it is too late for the estimate to influence anything.

Two components, same contract, different weight

Take the two purchases actually on the table this quarter: a managed LLM gateway and a managed vector database, both month-to-month, both with published REST APIs and maintained SDKs. On lock-in they score identically, which is to say near zero. On switching cost they are not close.

DimensionManaged gatewayManaged vector database
State it holds that is yoursRequest logs, cost records, key materialThe corpus: 4.2M embedded chunks plus the index built over them
What an export gives you backLogs as CSV, which you mostly do not needVectors and metadata, which is the cheap part
Code written against its semanticsOne HTTP client and a routing configEvery query builder, every metadata filter, hybrid search weighting, reranker thresholds
Tuning investedRetry and failover policy, a few hoursIndex parameters chosen over ~6 weeks of recall and latency trials
Quality couplingNone — same prompts, same models, same outputsDirect — retrieval changes what the model sees, so answer quality moves
Blast radius of getting it wrongRequests fail loudly and you roll backAnswers get quietly worse and nobody notices for a fortnight
Estimated exit at 18 months2–3 engineer-weeks10–14 engineer-weeks plus 6 weeks dual running

The gateway is cheap to leave because its interface is narrow and its state is disposable. You write an adapter, re-point a base URL, replay a week of traffic in shadow — the new vendor serving a copy of live requests whose responses are recorded and compared rather than returned to anyone — and compare error rates; if it is wrong you find out in minutes because requests fail. The vector store is expensive to leave because retrieval quality is a property of the whole assembly — chunker, embedding model, index parameters, filter semantics, reranker, and the prompts written around what comes back — and swapping one component changes the behaviour of the assembly without changing any of the code around it.

The two-to-three weeks for the gateway is worth itemising, because "cheap" is a comparison rather than a synonym for free. Three days to write an adapter against the new provider's routing configuration and reproduce the failover policy; two days to move key material and rotate it; three days to re-point observability, since dashboards and alerts are keyed on the old vendor's span attributes and every one of them breaks; four days of shadow traffic at 5% while comparing error rates and p95 latency; two days for cutover and decommissioning. Twelve working days, and the reason it stays at twelve is that every one of those tasks fails loudly. A misrouted request throws. A bad key returns 401 on the first call. Nothing about the gateway can be wrong quietly.

Retrieval can be wrong quietly, and that asymmetry is most of the difference between the two rows in the table. A migration that leaves recall@10 at 0.87 instead of 0.94 produces no errors, no alerts and no failed tests; it produces answers that cite the second-best clause. Whether anyone notices depends entirely on whether the team owns an evaluation set good enough to measure it, which is why the exit cost of a vector store is partly a function of how well instrumented the retrieval layer was before anyone thought about leaving.

What the export does not carry

The concrete parts are worth seeing, because "it is only an API" survives right up until you compare the two APIs on a real query.

python
# What eighteen months of tuning looks like on the incumbent
results = store.query(
    vector=q_vec,
    top_k=40,                                    # 40 because post-filtering drops candidates
    filter={
        "$and": [
            {"tenant_id": {"$eq": tenant}},
            {"doc_type": {"$in": ["contract", "amendment"]}},
            {"effective_from": {"$lte": as_of_epoch}},
        ]
    },
    alpha=0.35,          # hybrid: 0.35 dense / 0.65 lexical, tuned on the golden set
    include_metadata=True,
)
candidates = [r for r in results if r.score >= 0.62]   # threshold set against THIS metric
reranked = reranker.rank(query, candidates)[:8]
python
# The replacement, same intent, different everything
results = store.search(
    data=q_vec,
    limit=40,
    filter_expr='tenant_id == "%s" and doc_type in ["contract","amendment"] '
                'and effective_from <= %d' % (tenant, as_of_epoch),
    search_params={"metric_type": "IP", "params": {"nprobe": 32}},
)
# no hybrid alpha: lexical is a separate collection you must query and fuse yourself
# scores are inner-product, not cosine — 0.62 means nothing here

The filter expression is a string instead of a dict, which is an afternoon. The metric is inner product rather than cosine, so every threshold tuned against the old scale is now a number with no meaning, and the reranker cutoff at 0.62 either passes everything or nothing until someone re-derives it. The incumbent applied the filter after the vector search and the replacement applies it before, so top_k=40 no longer produces the same candidate distribution and the 40 itself has to be re-chosen. Hybrid search was one parameter and is now a second collection, a second query, and a fusion function you write and tune. And the index parameters — M=32, ef_construction=256, ef_search=128, arrived at over six weeks of trials that landed on p95 41 ms at recall@10 of 0.94 — do not transfer to a store whose index is IVF-PQ with nlist and nprobe. You run the trials again.

Re-embedding, which everyone expects to be the expensive part, usually is not, at least in money. Re-embedding 4.2M chunks averaging 512 tokens is about 2.15 billion tokens; at roughly £0.02 per million that is £43 and a nine-hour run. What costs is that the pipeline which produced those chunks eighteen months ago has drifted, the chunker version that made them is not recorded anywhere, and reproducing the corpus exactly is now an archaeology exercise rather than a job you re-run. Costed out, the eleven weeks broke down like this.

WorkEngineer-weeks
Rewrite query layer, filters, hybrid fusion2.5
Re-derive thresholds and re-tune index parameters against the golden set3.0
Rebuild and validate the corpus, including chunker archaeology2.0
Shadow traffic, compare retrieval quality, fix the regressions found2.5
Cutover, rollback plan, decommission1.0

Plus six weeks of double licence spend at roughly £3,400 a month, and a period during which nobody could ship retrieval features because the team was holding two implementations in their heads.

Writing it down before you sign

The practice that makes the difference is small and unglamorous: at signing, write one paragraph estimating what leaving this component costs, in weeks, with the line items named. It takes an hour, it is wrong by a factor of two, and it still changes decisions, because the act of writing "10–14 weeks" next to a £3,400-a-month invoice puts the two on the same page for the first time. Re-estimate it annually alongside the renewal, since switching cost grows with use and the estimate at month one is the cheapest it will ever be.

Four engineering habits do more to lower that number than any amount of contractual language. Keep the source of truth for chunks outside the vector store, in your own object storage or Postgres, with the chunker version and embedding model id stamped on every record, so the corpus is reproducible rather than recoverable. Own the golden evaluation set, so that when retrieval changes you can measure the delta instead of arguing about it. Keep query construction behind a narrow port, in the sense that chapter 1's layering already assigns to provider clients, so the engine-specific string appears in one file. And record the tuning decisions with the reasoning — that top_k=40 exists because of post-filtering, that 0.62 is a cosine threshold — because a magic number with a comment is portable and a magic number without one is not.

A small number of contract terms are worth asking for at signing, and they are cheap to get then and impossible to get later. An export endpoint with a stated throughput and a stated format, rather than a promise of "assistance with migration", because a support-ticket-driven export of 26 GB is a very different thing from a documented API. A cap on renewal uplift, since the price you are modelling over three years is only the first year's. A commitment to delete your data within a stated window after termination, which your own legal and security review will need anyway. And clarity on whether the index parameters and any vendor-side tuning are yours to see; a vendor that will not tell you what M and ef_search are set to has made your tuning unreproducible on purpose.

Switching cost also grows in a shape worth knowing. It is roughly flat for the first few months, when the corpus is small and the query layer is one file, and then rises with every feature that reaches into the engine's semantics — a new filter dimension, a reranking stage, a caching layer keyed on the engine's scores. The practical consequence is that the cheapest moment to leave is early, which is when nobody wants to, and the moment everyone wants to leave is late, which is when it costs the most. A team that expects to be unsure for a while should therefore spend its first month on the reproducibility habits above rather than on features that deepen the coupling, and should schedule the first re-estimate of exit cost at month six rather than at renewal.

What does not work, and is the tempting version, is building a generic abstraction over vector stores on day one so that you are portable by construction. The abstraction can only expose the intersection of what all the engines do, which strips out precisely the capabilities you are paying a managed vendor for — filter push-down, native hybrid search, integrated reranking — and leaves you with a worse retrieval system permanently in exchange for a migration you will probably never run. It also does nothing about the expensive part, since the corpus, the thresholds and the index trials are unaffected by how tidily your code calls the API. Reproducible inputs are the cheap insurance; a universal adapter is expensive insurance against the wrong risk.

None of this argues for avoiding switching cost. A managed vector store that gives you hybrid search and filter push-down on day one is worth real money, and accepting fourteen weeks of future exit cost to get eighteen months of leverage now is a perfectly good trade — as long as it is a trade somebody made deliberately, with the fourteen weeks written down, rather than a surprise discovered by the engineer who volunteered to do the migration.

The natural conclusion to draw from a chapter about switching cost is that the way to owe nobody anything is to build every layer yourself, and there is always a senior engineer ready to make that case for the layer they find most irritating. The next chapter is about which layer, if any, deserves it, and why the irritation is the least reliable signal available.

Build vs buy across the stack — read the current version at https://daviddominguez.dev/learn/en/build-vs-buy-across-the-stack/the-contract-ends-in-a-month-the-dependency-does-not

Back to top