Skip to content

What SDK Version Pinning Actually Protects You From

10 min read · updated August 11, 2026

“Pin your dependencies” is advice everybody accepts and few people can say the consequences of. A pin does not make your integration stable. It makes the instability arrive on a date you picked, in a batch you chose the size of, and only across the surface the pin actually covers — which, for an LLM client, is the smaller half.

The claim, stated precisely

A version pin is a scheduling device. Unpinned, a change in somebody else’s code reaches your production at whatever moment your next deploy happens to install it — which is to say, at a moment chosen by the intersection of their release calendar and your deploy calendar, and observed by nobody. Pinned, the same change reaches you when you edit a number in a manifest, which is a moment you chose, in front of a test suite you were watching.

The total quantity of change is identical. What differs is whether it arrives observed. That is a large benefit and it is worth the friction, but it is not the benefit people describe when they say pinning makes things safe, and the difference matters because the gap between those two claims is where outages live.

What a pin does protect

The category worth pinning against is not the loud breakage. A removed method is loud: the process fails to import, CI goes red, nothing ships. Pinning helps there, but so does any test at all.

The category that justifies the pin is silent default change, where the code still runs and does something different. In an LLM client, defaults reach further into behaviour than in most libraries:

  • Retry and timeout defaults. Official clients retry some failures for you and impose a request timeout. If a minor bump changes the default retry count or the default timeout, your p99 latency and your bill both move, and no line of your code mentions either number. Nothing in your diff explains the graph.
  • Parameter defaults sent on your behalf. A client that starts sending a field it previously omitted — a streaming option, a store flag, a tool-choice default — changes what the server does without changing what you wrote.
  • Response object surface. A field that becomes optional, a convenience accessor whose type widens, an enum that gains a member. Typed languages catch some of this at build time; Python and JavaScript often catch it at runtime on the one request that hits the new branch.
  • Error taxonomy. If exception classes are reorganised, an except or catch clause that used to match a rate-limit error stops matching. Your backoff logic quietly becomes a crash, and only under load, which is exactly when you cannot afford to find out.
  • Streaming iteration shape. A change in what the stream helper yields — raw events versus accumulated state — turns a working consumer into one that renders nothing while reporting no error.

Every item on that list produces a production symptom with no corresponding change in your repository. That is what makes it worth the ceremony: not that the change is dangerous, but that it is unattributable. An incident you cannot bisect costs several times what the same incident costs when a version number in the diff explains it.

What a pin cannot protect

Here is the argument’s other half, and the reason a pin is risk-moving rather than risk-removing.

It does not pin the model. The client SDK is your side of the wire. The weights are the other side. A pinned SDK calling a floating model alias gets whatever the provider points that alias at today, and the library already documents what that costs you in silent model updates and the regression that follows one. Of the two clocks, this is the one that changes outputs, and the pin people are proud of does not touch it.

It does not pin the endpoint. Servers deploy continuously. A pinned client can send an identical request and get a new field back, a changed default applied server-side, or a deprecation warning header. Where a provider offers an API version as a header or a query parameter — Anthropic documents an anthropic-version request header, and Azure OpenAI documents an api-version query parameter — that is a separate pin, and the one that actually freezes the contract you are coding against. An SDK pin without an API-version pin freezes the caller and leaves the callee free.

It does not stop time. A pin held for eighteen months is not eighteen months of safety; it is eighteen months of deferred reading, redeemed in one sitting when a security advisory or a deprecated endpoint forces the upgrade. The cost of an upgrade is not linear in the number of versions skipped, because breaking changes compose: a migration guide written for one major assumes you are on the previous one. Skipping three majors means reading three guides in sequence and testing the composition of all of them, and the composition is what nobody wrote a guide for.

It does not protect a transitive dependency you did not pin. Pinning the LLM client while an HTTP library underneath it floats leaves the connection pool, the TLS behaviour and the timeout semantics unpinned. This is what a lockfile is for, and a manifest pin without a committed lockfile is a pin on one node of a graph.

Three clocks, not one

The practical content of this argument is that an LLM integration has three independent version clocks, and treating them as one is the mistake underneath most “we pinned everything and it still broke” stories.

  • Client SDK version. Changes how your request is constructed and how the reply is parsed. Fails loudly or changes defaults. Under your control entirely.
  • API version. Changes what the endpoint accepts and returns. Under your control only where the provider exposes a version selector; otherwise it moves and you find out from a changelog.
  • Model version. Changes the answers. Under your control if you name a dated model string rather than an alias, and only until that dated string is retired — the library covers those dates in model deprecation dates.

They fail differently and so they want different policies. Conflating them produces the two symmetric errors: a team that pins the SDK hard and lets the model float, and wonders why output quality moved with no deploy; and a team that pins a dated model string and lets the SDK float, and wonders why latency moved with no model change.

The policy this argument implies

If a pin buys scheduling rather than safety, then the policy question is not “pin or not” but “how often do you want to pay, and in what size instalments”. The position this page defends:

  • Exact versions in the lockfile, committed, and installed from in CI and in the image build. A lockfile that is not what production installs from is decoration.
  • A range in the manifest that cannot cross a major.Patch and minor upgrades still arrive, but through a pull request with a diff, from an automated bot, on a schedule — not through a rebuild.
  • Pin the API version explicitly where the provider offers one. It is one header and it is the only pin that freezes the contract rather than the caller.
  • Pin the model string separately and deliberately, with its own review cadence, because it is the clock that moves output quality and it is the one your users feel.
  • Upgrade on a cadence rather than on an incident.Monthly is not a magic number; the point is that the interval is chosen, so the batch stays small enough that a bisect is one step.

Every one of those is a decision about when to pay, not a way to avoid paying. That is the whole argument. Pinning is worth doing precisely because it converts an unattributable production change into a scheduled reading task — and it is worth being honest that the reading task is the price, because teams that expected safety instead of scheduling are the ones who never do the reading and end up doing all of it at once.

The semantic versioning specification defines what a major bump is permitted to mean, which is worth reading once: it constrains what a publisher may put in a minor release, but it says nothing about defaults, and a default change is not an API change under that definition. That gap is precisely the gap this page is about.