Vendor Lock-In in Code Execution Infrastructure
Somebody on the buying side eventually asks the question, usually in a security review, usually phrased as "what's our exit plan." The vendor answers "we're open standards, it's just an API," everyone writes that in the doc, and the doc gets approved. Two years later the migration estimate comes back at four months and nobody can explain the gap between the answer and the estimate.
The gap is that "lock-in" is four different things with wildly different costs, and the cheapest of the four is the only one anybody actually discusses. I sell a code-execution platform, which means writing this honestly is either a bad idea or the only useful version of it. I'll take the second, including an explicit accounting of where PandaStack locks you in — because a lock-in post from a vendor that concludes "and that's why our platform has no lock-in" is worth nothing, and you'd be right to stop reading at that sentence.
The four kinds, from trivial to inescapable
Rank them by what it costs to leave, not by how much they get talked about. The ranking is roughly inverse to the amount of attention each one gets.
Interface lock-in: you coded against their SDK
This is the one everybody means when they say lock-in, and it is the cheapest by an order of magnitude. You imported someone's client library, you called create and exec and kill, and now their type names are in your codebase. Unwinding it is an adapter: one file, one interface, a couple of days including tests.
The reason it feels expensive is not the API surface, it's the sprawl. If eighty files import the vendor's SDK directly, the adapter is not one file, it's eighty edits and a merge conflict with every open branch. The cost is entirely a function of how many places you let the import reach, which is a decision you make on day one and cannot make retroactively for free. Every code-execution SDK on the market is a thin wrapper over a REST API that does about six things; the layer that makes them swappable is small, and you either wrote it early or you didn't.
Data lock-in: your state is in their format, in their bucket
Now it costs real money. Snapshots, volumes, images, logs, metrics, audit trails, the metadata that says which sandbox belonged to which customer. Some of that is portable by nature and some of it is a proprietary binary blob whose only reader is the vendor's own runtime.
The test that separates them is unglamorous: can you get the bytes out over a documented API, and once you have them, is there anything on earth that can read them? Those are two different questions, and vendors answer the first one when you ask the second. A snapshot you can download but not restore anywhere else is an export in the same sense that a photograph of a house is a house.
Semantic lock-in: you built on a behaviour that has no equivalent
This is the real one, and it is almost never in the vendor questionnaire, because there is no field for it. Semantic lock-in is when your product's design depends on a capability the market does not generally offer, so there is no target to port to — the exit is not a migration, it's a redesign.
Concretely: you built an agent that explores several branches of a plan in parallel by forking a running sandbox — same memory, same open files, same half-installed dependency tree — and picking the winner. That is a sub-second copy-on-write operation on a platform that offers it. On a platform that doesn't, the equivalent is "boot four fresh containers and replay the setup in each," which is not slower by a factor of two, it's slower by a factor that makes the feature not worth shipping. Your product doesn't degrade when you migrate. It stops existing.
Interface lock-in costs you an afternoon. Data lock-in costs you a quarter. Semantic lock-in costs you the feature you built the company on, which is why no vendor puts it on the comparison page and every vendor sells it as an advantage.
The uncomfortable part is that semantic lock-in is not a trick played on you. It is what buying a differentiated platform means. The capability that makes the vendor worth paying for is, definitionally, the capability you can't get elsewhere. You can avoid it entirely by only ever using features that every competitor also has, and I'll come back to what that costs, because it isn't free either.
Operational lock-in: your runbooks assume their control plane
The quietest of the four and frequently the largest line in the actual migration plan. Your on-call runbooks reference their dashboard. Your alerts are built on their metric names. Your incident review template has a step that says "check the vendor status page." Your SOC 2 evidence includes their subprocessor listing and their pen-test report. Your support team has muscle memory for their error codes.
None of that is in your repository, so none of it shows up when an engineer estimates the migration by grepping for the SDK import. It shows up eight weeks later, when the new platform works fine and nobody knows what to do at 3am. Budget for it by counting artifacts outside the codebase that name the vendor: dashboards, alert rules, runbooks, compliance documents, onboarding docs, the Slack channel where the integration is discussed.
The exit-cost test: two questions and how to grade the answers
You do not need a forty-item vendor questionnaire. You need two questions, asked of a human who knows the system, and the willingness to grade the answer instead of filing it.
Question one: what does my data look like on the way out? Not "do you have an export API" — a yes there is nearly meaningless. Ask what format, whether the format is documented, and whether anything other than their runtime can consume it.
- A grade — your workload state is in artifacts you already own: an OCI image, a git repo, an object-storage bucket you control, a Postgres you can pg_dump. The vendor holds derivatives it can rebuild, not originals it alone can read.
- B grade — everything is downloadable in a documented format, but the useful thing (a running-state snapshot, an internal image layer) needs their runtime to be worth anything. You can leave; you rebuild from source rather than restore.
- C grade — export exists, is undocumented, and no engineer can tell you offhand whether anyone has ever run it. This is the modal answer and you should treat it as a no.
- F grade — the answer is a support ticket, a professional-services quote, or the phrase "we can arrange that for enterprise customers." Exit priced on request is exit priced when you are least able to negotiate.
Question two: can I run the runtime myself? Not will I, not do you recommend it. Can I. The answer bounds every future price negotiation whether or not you ever exercise it, in the same way a tenant's ability to move bounds rent increases without anyone actually moving.
Grade this one on specificity. "Yes, here's the repo, here's the license, here's the doc, here are the things that don't work in self-hosted mode" is an A, and the list of things that don't work is the part that proves the answer is real. "Yes, we're open source" with no follow-up is a C until you have checked what is actually in the repository. "No" is an honest F and considerably better than a dishonest B — at least you can price it.
Why an open-source core changes the arithmetic, and when it doesn't
An open-source runtime caps the vendor's pricing power even if you never self-host a single node. That's the whole mechanism, and it's worth being precise about it because the usual arguments for open source (community, auditability, values) are real but are not the one that matters to a buyer.
The mechanism is that a credible self-host option puts a ceiling on what the managed version can charge. The ceiling is not the vendor's cost, it's your cost of running it yourself plus the value of not having to — and that number, whatever it is, is finite and knowable. Compare a closed platform, where the ceiling is whatever you can be persuaded to pay before a redesign is cheaper. Buyers who have never once considered self-hosting still benefit from the option existing, which is a strange and underrated property.
But "open source" on a marketing page means nothing until you check that the open part is the part you depend on. The failure modes are consistent enough to enumerate.
- The SDK is open, the runtime is not. Extremely common and close to worthless — you were never locked in by the client library, which is the cheap kind of lock-in from the top of this post.
- The runtime is open, the control plane is not. Better, but the control plane is where scheduling, quotas, auth and multi-tenancy live. You can run one node; you cannot run a platform.
- It's open, and it's a snapshot from fourteen months ago. Check the commit dates and whether the public tree matches the shipped version. A repository that has stopped moving is a museum, not an exit.
- It's open, and building it requires artifacts only they can produce. If self-hosting needs a proprietary base image, a signed kernel, or a template bundle from their bucket, the license is open and the system is not.
- It's open under a license with a competition clause. Fine for a self-hosting user, disqualifying if your product embeds it. Read the actual license and not the badge — the badge is generated from a string in a file.
The check takes twenty minutes: clone the repo, read the license, look at the commit graph, and try to identify the specific commit that corresponds to what you're being sold. If you can't do that last one, you've learned something more useful than anything on the pricing page.
The portability layer that actually earns its keep
There is a version of portability engineering that pays for itself in a week and a version that quietly eats a quarter. The difference is whether you're abstracting the vendor or abstracting the domain. Four rules, in descending order of value per hour spent.
One: keep the workload a plain OCI image, or a plain repo plus a start command. This is the single highest-leverage thing on the list and it costs almost nothing, because it's what you'd do anyway. A Dockerfile is a portable artifact by construction. The moment your build depends on a vendor-specific manifest format or a proprietary layer cache, your workload stops being a thing that runs elsewhere and becomes a thing that runs there.
Two: keep secrets in your own store and state in your own Postgres or bucket. If the authoritative copy of anything lives only inside the vendor's boundary, you have converted interface lock-in into data lock-in, which is a category upgrade in the wrong direction. Sandboxes should hold derivatives — a checkout, a build tree, a running process — never originals.
Three: treat the sandbox API as a thin adapter behind your own interface. Not a full abstraction layer with a plugin system and a capability negotiation protocol — an interface with four methods that expresses what your application actually needs, and one file per vendor that satisfies it.
// runtime.ts -- the ENTIRE vendor surface your application is allowed to see.
// Four methods. If it grows past about eight, you are abstracting the vendor
// instead of your own domain, and the abstraction will leak anyway.
export interface ExecResult {
stdout: string;
stderr: string;
exitCode: number;
}
export interface Handle {
readonly id: string;
}
export interface ExecRuntime {
/** image: an OCI reference or template name. Yours, not theirs. */
start(opts: {
image: string;
ttlSeconds?: number;
labels?: Record<string, string>;
}): Promise<Handle>;
exec(h: Handle, cmd: string, opts?: { timeoutSeconds?: number }): Promise<ExecResult>;
stop(h: Handle): Promise<void>;
}// adapters/pandastack.ts -- one file. Swapping vendors is replacing this file,
// not editing every caller. Note what the adapter does: it normalises the bits
// that differ between SDKs (snake_case vs camelCase exit codes, handle types)
// and refuses to leak anything else upward.
import { Sandbox } from "@pandastack/sdk";
import type { ExecRuntime, ExecResult, Handle } from "../runtime";
export class PandaStackRuntime implements ExecRuntime {
private live = new Map<string, Sandbox>();
async start(opts: {
image: string;
ttlSeconds?: number;
labels?: Record<string, string>;
}): Promise<Handle> {
const sbx = await Sandbox.create({
template: opts.image,
ttlSeconds: opts.ttlSeconds ?? 300,
metadata: opts.labels,
});
this.live.set(sbx.id, sbx);
return { id: sbx.id };
}
async exec(h: Handle, cmd: string, opts: { timeoutSeconds?: number } = {}): Promise<ExecResult> {
const r = await this.get(h).exec(cmd, { timeoutSeconds: opts.timeoutSeconds ?? 60 });
return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exit_code };
}
async stop(h: Handle): Promise<void> {
const sbx = this.live.get(h.id);
if (!sbx) return; // already reaped by TTL, which is not an error
try {
await sbx.kill();
} finally {
this.live.delete(h.id);
}
}
private get(h: Handle): Sandbox {
const sbx = this.live.get(h.id);
if (!sbx) throw new Error("unknown handle: " + h.id);
return sbx;
}
}
// adapters/docker.ts -- the fallback that proves the interface is honest.
// Same four methods over "docker run -d" / "docker exec" / "docker rm -f".
// It is much slower and much less isolated, and that is fine: its job is to
// be a second implementation, and a second implementation is the only thing
// that ever proves an abstraction actually abstracts.The rule that makes this hold over time is a lint rule, not a design doc. One grep in CI — the vendor's package name must not appear outside the adapters directory — and the discipline maintains itself. Without it, someone needs a feature the interface doesn't expose, imports the SDK directly "just here," and eighteen months later you are back to eighty files.
Four: rehearse the export before you need it. An export path nobody has run is a claim, not a capability, and this is as true of your own code as of the vendor's.
# export_drill.py -- run this quarterly, in CI if you can, and make it fail
# loudly. The point is not the tarball. The point is discovering, on a normal
# Tuesday, that the thing you assumed was exportable is not.
from pandastack import Sandbox
sbx = Sandbox.create(template="base", ttl_seconds=300)
try:
sbx.exec("mkdir -p /workspace && echo 'irreplaceable' > /workspace/state.txt")
# 1. Can I get the bytes out over a documented API, without a support
# ticket and without anyone's permission?
sbx.exec("tar -C /workspace -czf /tmp/export.tgz .")
sbx.filesystem.download("/tmp/export.tgz", "./export.tgz")
# 2. Can anything OTHER than this vendor's runtime read what I just got?
# A tarball: yes, trivially, forever.
# A proprietary running-state snapshot: no, and that is the whole
# difference between grade A and grade B on the exit-cost test.
finally:
sbx.kill()
# The uncomfortable follow-up question, which you should also answer in
# writing: how long does a full rebuild-from-source take for every workload
# currently running, if the platform disappears tonight? If nobody knows,
# that number is your real exit cost and it is not on any invoice.Four ways to run untrusted code, scored on all four dimensions
Scored low to high on each axis, where low is good. The interesting result is that no column is clean — build-it-yourself trades vendor lock-in for lock-in to your own decisions, which is still a cost, just one you pay in headcount instead of invoices.
- Build it yourself on raw Firecracker — Interface: none, it's your code. Data: low, you chose the formats and own the bucket. Semantic: low against vendors, but high against your own past decisions — your snapshot layout and network model are now load-bearing and nobody else maintains them. Operational: highest of the four, because you are the control plane, the on-call and the compliance evidence. Verdict: no vendor lock-in, maximum total cost of ownership; sensible if execution infrastructure is your product.
- Managed sandbox API (this category, including PandaStack) — Interface: low, an SDK over a REST API, one adapter file. Data: medium, depends entirely on whether snapshots and images are yours or theirs; ask the two questions. Semantic: high, and rising with every differentiated feature you adopt — this is what you are paying for. Operational: medium, their dashboard and their status page enter your runbooks. Verdict: the fast path, with an exit cost proportional to how much of the differentiated surface you used.
- Serverless FaaS (Lambda, Cloud Run, Workers) — Interface: medium, the handler signature and the deployment format reach further into your code than an SDK does. Data: low to medium, the code is yours but the packaging and the event contracts are not. Semantic: high in a way people underrate — the execution model itself (time limits, no persistent local state, cold-start behaviour, restricted syscalls) is baked into every line you wrote. Operational: medium to high, deeply entangled with one cloud's IAM and observability. Verdict: cheapest to start, and the migration is a rewrite of the shape of your code rather than a swap of a client library.
- General cloud VM (EC2, GCE, a rented box) — Interface: near zero, it's Linux. Data: low, plain disks and plain images. Semantic: low, there is no exotic behaviour to depend on, which is exactly the point and exactly the limitation. Operational: high, you own patching, isolation, scheduling and capacity. Verdict: maximum portability, and you have bought a machine rather than a platform — every feature above 'a Linux box' is yours to build.
What refusing every platform feature actually costs
The logical endpoint of maximal portability is that you only use capabilities every candidate offers, which means you have designed to the lowest common denominator. In this category, the lowest common denominator is a container that takes on the order of ten seconds to become useful — pull, start, install, warm — and offers isolation that is, let's be generous, a strongly worded suggestion to a shared kernel.
For an interactive agent product that ten seconds is not a performance note, it's the product. A user who waits ten seconds for a sandbox before anything happens is a user forming an opinion about your latency. And a shared kernel is a genuinely different security posture from a hardware-virtualised boundary when the code you're running was written thirty milliseconds ago by a language model that has never been held accountable for an rm -rf.
So the honest framing isn't "avoid lock-in." It's "know which lock-in you bought and what it bought you." Adopt the differentiated feature deliberately, write down what depends on it, and keep the dependency in a place you can find later. A capability you chose with a note in the design doc is an engineering decision. The same capability, adopted because it was convenient in a sprint, is a surprise in a due-diligence meeting.
Where PandaStack locks you in, specifically
Applying the same four categories to my own product, because a post that grades other vendors and exempts itself is an advertisement wearing a lab coat.
Interface: low, and I don't get credit for that. Our SDK is a wrapper over a REST API with roughly the operations in the adapter above. If you write that adapter file you can point it somewhere else in an afternoon. This is the kind everyone worries about and it is the kind that doesn't matter.
Data: medium, and I want to be precise about which half is which. Your files are files — download them over the filesystem API, they're a tarball, done. Your templates start life as a Dockerfile or an OCI image reference, so the input to a template is a portable artifact that runs anywhere Docker runs. What is not portable is a snapshot. A PandaStack snapshot is a Firecracker memory image plus a serialised VM state blob plus a metadata file, and that metadata records things like the guest's baked MAC address, its IP, and the host path of the rootfs the state blob references. That is not us being coy — it's what a hypervisor snapshot is. Restoring one requires reconstructing the network identity it was frozen with, which is fiddly enough between two of our own hosts that we wrote code specifically to handle it. There is no world in which you restore that on someone else's platform. If your architecture assumes long-lived snapshot lineages, that state is genuinely stuck, and the mitigation is to be able to rebuild each sandbox from a repo and a template rather than from a snapshot chain.
Semantic: high, and this is the one I'd flag in a review if I were on your side of the table. Sub-second copy-on-write forking of a running microVM has no drop-in equivalent — not a slower equivalent, an absent one. If you build branch-and-explore agent behaviour on it, you have built on something you cannot buy elsewhere today, and the honest description of your exit is a redesign of that feature, not a migration of it. The same goes for restore-on-create boot times: design a UX around a create that returns in a couple of hundred milliseconds and the fallback is not a slightly worse UX, it's a spinner and a different product.
Operational: medium, and identical to every managed platform. Our dashboard, our metric names and our status page end up in your runbooks. Nothing clever to say about it beyond: count those artifacts when you estimate a migration, because they are invisible to a grep.
The mitigation I'd actually stand behind is narrow and I'd rather state it narrowly than oversell it. Workloads on PandaStack are plain Linux: a repo, a Dockerfile, a start command, files on a normal filesystem. The runtime is Apache-2.0 and the agent and control plane are in the repository, which means self-hosting is a real option and therefore a real ceiling on what the managed version can charge. So the work is portable even when the speed is not. Your code runs somewhere else tomorrow; it just runs slower, and the features that depended on the speed don't come with it. That's a smaller promise than "no lock-in," and it has the advantage of being true.
The checklist, compressed
If you keep one thing from this post, keep the ordered version.
- Write the adapter on day one. Four methods, one file per vendor, and a CI grep that fails the build if the vendor package is imported outside it. This is an hour of work that converts the loudest category of lock-in into a non-issue permanently.
- Keep originals outside the vendor. Secrets in your store, state in your Postgres or bucket, source in your git. Anything the sandbox holds should be reconstructible from something you own.
- Keep the workload an OCI image or a repo plus a start command. Portable by construction, and it costs nothing because it's what you were going to do anyway.
- Ask the two questions out loud, of an engineer. What does my data look like on the way out, and can I run the runtime myself. Grade the answers rather than filing them.
- Verify the open-source claim against the repository. License, commit dates, and whether the open part is the part you depend on. Twenty minutes, and it changes the negotiation.
- Name the semantic dependencies deliberately. Write down each platform-specific capability you rely on and what your product does without it. If the answer is 'not exist', that is fine — it just needs to be a decision with a date on it.
- Model the bill at 10x and 100x volume. Pricing structure is a lock-in dimension, and it is the one that changes character with scale rather than with time.
- Rehearse the export quarterly and make the drill fail loudly. Every untested export path is a claim, and claims have a poor record under pressure.
None of this adds up to avoiding lock-in, because you can't, and a strategy whose goal is zero lock-in produces a system built from the least interesting capabilities available. What it adds up to is knowing the number. Interface lock-in: an afternoon, and only if you skipped step one. Data lock-in: whatever a full rebuild costs, which you should have measured. Operational lock-in: eight weeks of unfamiliarity, roughly. Semantic lock-in: the feature, priced as a redesign.
Add those up before you sign and you're making a trade. Skip the exercise and you're not avoiding the cost, you're just deferring the moment you find out what it is — traditionally to a quarter when you have less leverage, less time, and an audience.
Frequently asked questions
What is the difference between interface lock-in and semantic lock-in?
Interface lock-in is that your code imports a specific vendor's SDK and calls their method names. It is cheap to unwind — you define your own small interface, write one adapter file per vendor, and the rest of your codebase never knows which one is behind it. A couple of days including tests, and near zero if you did it up front. Semantic lock-in is that your product depends on a behaviour the vendor offers and nobody else does. The canonical example in code-execution infrastructure is sub-second copy-on-write forking of a running sandbox: you can express the intent in any API you like, but no adapter can make a competitor perform an operation their runtime does not implement. Migrating away from interface lock-in is a refactor. Migrating away from semantic lock-in is a redesign of the feature that depended on it, and sometimes the honest conclusion is that the feature does not survive the move. This is the dimension least discussed in vendor evaluations and usually the largest real cost.
How do I test whether a vendor's data export is real?
Ask two separate questions, because vendors reliably answer the first when you ask the second. First: can I get the bytes out over a documented API without a support ticket? Second: once I have those bytes, can anything other than your runtime read them? Plain files, tarballs, OCI images, SQL dumps and object-storage buckets pass the second test. Proprietary running-state snapshots, internal image layer formats and undocumented metadata blobs fail it — you can download them and they are useless outside the platform that wrote them. Then run the drill rather than believing the answer: create something, export it, and try to consume the export somewhere else. Do it quarterly and in CI if you can. An export path that nobody has executed is a claim, and claims tend to be discovered as false at exactly the moment you are relying on them.
Does an open-source core actually protect me if I never self-host?
Yes, through a mechanism that has nothing to do with you running the software. A credible self-host option puts a ceiling on what the managed version can charge, because the vendor's pricing is bounded by your cost of running it yourself plus the value of not having to. That is a finite, knowable number. With a closed platform the ceiling is instead whatever you can be persuaded to pay before a redesign becomes cheaper, which is a much higher and much vaguer figure. The protection is real even if you never exercise it — but only if the open part is the part you depend on. Verify: is the runtime open or just the SDK? Is the control plane included, or only a single node? Do the commit dates suggest the public tree matches what you are being sold? Does building it require artifacts only the vendor can produce, like a proprietary base image? And read the actual license rather than the badge, because a license with a competition clause is fine for a self-hosting user and disqualifying if your product embeds the code.
Is a vendor abstraction layer worth building, or is it premature?
A small one is worth building immediately; a large one is usually a mistake. The version that pays for itself is an interface with about four methods — start, exec, stop, and whatever one thing your domain genuinely needs — expressed in your application's vocabulary rather than the vendor's, with one adapter file per backend. That takes an hour and permanently converts the loudest category of lock-in into a non-issue. The version that eats a quarter is a general-purpose abstraction over every feature every vendor offers, with capability negotiation and plugin registration. It leaks anyway, because the differentiated capabilities are precisely the ones that do not abstract cleanly, and it makes those capabilities awkward to use — so you pay twice. Two practical rules: enforce the boundary with a CI grep that fails if the vendor package is imported outside the adapters directory, and write a second adapter even if you never ship it. A second implementation is the only thing that ever proves an abstraction abstracts.
What does portability cost me in a code-execution platform?
It costs you the differentiated capabilities, and in this category those are mostly latency and isolation. If you restrict yourself to features every candidate platform offers, you have designed to the lowest common denominator, which is roughly a container that takes on the order of ten seconds to become useful — image pull, start, dependency install, warm-up — and that shares a kernel with everything else on the host. For an interactive agent product, that startup time is not a performance footnote; it is what the user experiences as the product. And a shared kernel is a materially different security posture from hardware virtualisation when the code being executed was generated seconds ago by a model with no stake in the outcome. So the useful goal is not zero lock-in. It is to adopt each platform-specific capability deliberately, write down what depends on it and what your product looks like without it, and keep that list somewhere you will find it during a migration rather than discovering it there.
Keep reading
- Build vs buy: running Firecracker yourself — The column in the comparison table with no vendor lock-in and the highest total cost.
- How to vet a code-execution vendor on security — The other half of the diligence conversation, with the same 'ask an engineer' rule.
- Self-hosting a code execution sandbox — What exercising the self-host option actually involves, rather than just pricing it.
- Fork vs clone vs snapshot — The capability behind the semantic lock-in this post is most honest about.
- Sandbox pricing models compared — Modelling the bill at 10x volume, which is a lock-in dimension of its own.
- Open-source code sandboxes — Candidates for the 'can I run the runtime myself' question, with the licenses to check.
49ms p50 cold start. Fork, snapshot, and scale to zero.