Symptom
You fix a typo in an error message. Four days later a customer escalates: their alerting pipeline greps for the old string and has gone quiet.
Or you make a function faster, and a test suite that had been green for two years starts failing intermittently — not because the function is wrong, but because it used to be slow enough to hide a race.
Or you tighten a parser to reject a malformed field that the spec never allowed in the first place, and discover that a third of production traffic sends it.
In every case the same objection comes back from the team: that was never part of the contract. Correct, and irrelevant. That is the whole content of the two results in this post — one describing what users do, one describing the design habit that hands them more to do it with.
Statement
Hyrum’s law. With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviours of your system will be depended on by somebody.
Hyrum Wright, from a decade of trying to change things inside Google’s monorepo. It is an empirical claim, not a theorem, and it is explicitly false for small $N$ — that is what “sufficient number of users” is doing in the sentence.
Postel’s robustness principle. Be conservative in what you do, be liberal in what you accept from others.
Jon Postel, RFC 761, 1980, in the TCP specification. It is a design principle: justified by argument, never proved, and adopted nearly universally by everything built on the internet for the following twenty years.
They run together because Postel is the cause and Hyrum is the bill. A lenient implementation accepts inputs its specification does not describe. Accepting them makes them observable behaviour. Hyrum’s law then says that behaviour becomes load-bearing for someone. And a behaviour that is load-bearing for someone can no longer be tightened — including back to what the specification actually said. Neither result reads properly alone: Hyrum without Postel sounds like a complaint about users, and Postel without Hyrum sounds like good manners.
Argument
Hyrum’s law is empirical, so honesty requires saying what the argument is and is not. There is no proof. There is a mechanism, and it has two moving parts.
Observability is much wider than documentation. The set of things a caller can detect about your system includes every documented guarantee, and also: iteration order, error message text, which of two equal-priority results comes back first, wall-clock timing, allocation counts, log format, the exact capitalisation of a header, whether a field is absent or null, and whether an operation happens to be atomic today. The contract is a small subset of the observable surface, and nothing enforces the difference.
Users are selected, not sampled. If a given user depends on a given observable with some small probability $p$, the chance that no one among $N$ users depends on it is $(1-p)^N$, which goes to zero. With $N$ in the millions, an observable that one user in a hundred thousand would build on is depended on by dozens of people. This is the only quantitative content in the law, and it is back-of-envelope, not a derivation. Its value is that it identifies the variable that matters: $N$. A library with twelve users has slack that a library with twelve million does not, and the same engineering caution is prudent in one case and theatre in the other.
The Postel half has a real argument, and it was a good one in 1980. Networks were heterogeneous, implementations were buggy, and a strict receiver would have been an unreachable island. Leniency bought deployability at a moment when deployability was the binding constraint.
What that argument omits is the second-order effect. A lenient receiver does not merely tolerate a malformed sender; it removes the sender’s feedback signal. The bug is never found, the behaviour spreads, and the union of what implementations accept becomes the de facto specification — one nobody wrote, nobody can enumerate, and nobody can change. The classic sequence is HTML: browsers competed on tolerance for broken markup for a decade, and HTML5 eventually had to be written by reverse-engineering what they had converged on, then specifying the error handling exactly, byte for byte. The spec did not describe a language; it documented an accident.
Forbids
“Internal” as a technical guarantee. A comment, a leading underscore, or a line in the docs constrains nobody. Only the compiler, the type system, or the runtime constrains people. If a behaviour is reachable, it is part of your API.
Fixing a bug in a widely-used interface at zero cost. With enough users, the buggy behaviour is someone’s workaround. This is not hypothetical: Windows has shipped compatibility shims for individual applications for thirty years, including one that preserved a use-after-free that SimCity depended on, because “the new Windows broke SimCity” is a Windows problem no matter who wrote the bug.
Keeping an implementation detail an implementation detail. The pressure runs in both directions, and the second direction is the one people miss. CPython 3.6 changed its dictionary layout for memory reasons, and insertion-ordered iteration fell out as a side effect that the release notes explicitly warned against relying on. By 3.7 it was a guarantee in the language specification, binding every implementation of Python forever, because within one release cycle enough code depended on it that the alternative was breaking the ecosystem. Nobody decided to add that feature. It was observed into existence.
Interoperability through leniency. Postel-liberal implementations do not converge on the specification. They converge on whatever the dominant implementation accepts, which is why “works in Chrome” became a synonym for “correct” and why TLS 1.3 had to be redesigned mid-standardisation to look like a TLS 1.2 session on the wire — middleboxes had been liberal about what they accepted for so long that a genuinely new handshake was undeployable.
Does not forbid
It does not forbid ever changing anything. The most expensive misreading, and
the most common: teams that freeze an interface permanently and cite Hyrum. The
law is about cost, not impossibility. Python 3, Rust’s edition mechanism, and
Go’s math/rand/v2 are all deliberate breaks that shipped, because each one paid
the price knowingly and provided a migration path. Hyrum’s law tells you there
will be a bill; it does not tell you the bill exceeds the benefit. Deciding that
requires the number of affected users, which is measurable.
It does not apply equally at every scale. “With a sufficient number of users” is a real precondition. A five-person internal service adopting Google’s deprecation ceremony — a two-year notice period, a compatibility shim, a migration tool — has misapplied a law about $N$ in the millions to an $N$ of three, and has spent a quarter to avoid a Slack message.
Postel is not simply wrong. RFC 9413 is careful about this, and so should anyone quoting it be. HTTP’s tolerance of header case and surrounding whitespace, and browsers’ willingness to render imperfect markup, made the early web deployable by people who were not protocol engineers, and that was worth more than the strictness would have been. The modern case against Postel is narrower than the slogan: for protocols intended to evolve, leniency ossifies the extension points, and the failure mode is that you cannot ship version two.
Strictness does not get you out of it. Go randomises map iteration order
specifically so that no program can depend on it — an excellent move, and it
closes exactly one hole. Programs still depend on timing, on allocation counts,
on the text of error values, on goroutine scheduling. Java specified
String.hashCode in the language specification because by the time anyone
noticed, changing it was already impossible; specifying it was surrender written
as a guarantee. Strictness narrows the observable surface. Nothing closes it.
Boundary
The useful moves all follow from the mechanism rather than from the slogans.
- Make it unobservable, not undocumented. Randomising is the strongest tool available: Go’s map ordering, address-space layout randomisation, and TLS GREASE (RFC 8701), which sends deliberately unassigned values so that implementations must tolerate the unknown values a future extension will use. GREASE is the direct engineering answer to ossification — exercise the extension points continuously so they cannot rust shut.
- Reduce $N$. Unstable feature gates, nightly-only APIs, and genuinely private interfaces work because they hold the user count below the threshold where the law bites.
- Version instead of mutating. Rust editions and protobuf’s field-number discipline both let new behaviour coexist with old rather than replacing it.
- Turn the surprise into telemetry. Chrome’s use counters and deprecation reports measure who depends on what before the change ships, which converts Hyrum’s law from an ambush into a number.
- Be strict on receipt, and say so early. The modern protocol position, and it is a reversal of Postel rather than a refinement of it: reject what the spec does not allow, while the population of implementations is still small enough to fix.