What a protocol must publish, and what protocols forbid
What a protocol must publish
Error semantics as part of the contract
The most important thing a protocol can tell a caller is whether retrying is safe. Without that, the caller must choose between under-retrying (losing work) and over-retrying (duplicating work or amplifying an outage). So the error taxonomy of a protocol must at least separate:
| Class | Meaning | Caller should | Example |
|---|---|---|---|
| Retryable, transient | The problem may go away | Retry with backoff | HTTP 503 (unavailable), 429 (too many requests, often with a Retry-After header) |
| Terminal, deterministic | The same request will fail again | Do not retry; fix the request | HTTP 400 (bad request), 404 (not found) |
| Partial success | Some items worked, some did not | Handle each item | A batch API that reports a result per item |
Partial success must be explicit, not implicit. Where this is vague, every caller invents a rule of its own, usually by matching text in error messages, and the rules disagree. Retry logic and failure handling in every client depend on this: they cannot do their job if the interface will not say what happened.
Published limits as obligations
Rate limits, payload caps, result-size caps and timeouts. The enforcement lives in the server's flow-control and multi-tenancy machinery. But the publication is an interface artifact, and it changes behaviour. A documented limit lets the caller shape its traffic in advance. An undocumented limit is found by being throttled, usually at the worst moment. An unstated limit is an assumption the caller cannot verify, and it gets verified for them, under load, in production. A published limit also becomes a promise you can no longer quietly change, which is the real reason many limits are never published.
Versioning and deprecation
Schema compatibility inside one system is a different topic. This is the lifecycle of the interface itself: version numbers, dual-running, sunset policy. Because no change at a boundary is atomic (Principle A), every change needs an interval in which both contracts hold. So the real cost of a protocol change is:
Negotiation and expiring credentials
Content negotiation. Both parties announce what they can handle, and they settle on the intersection. For example, an HTTP client sends Accept: application/json and the server picks a format it supports. This is the only place here where agreement is reached at run time, rather than in advance (a registry) or never (text inference). It is a third option next to carried schemas and registry schemas. It matters because it is how a protocol stays compatible across versions without running everything twice. Its failure mode is distinctive: the intersection silently degrades to the weakest option both support, and nobody notices they are on the slow path. It sits next to versioning.
Authentication as an expiring, rate-limited dependency. Getting and refreshing a token is on the critical path of every integration, and it fails differently from everything else: not slowly, not partially, but as a cliff at a fixed time. The failure modes that appear nowhere else:
- a token expires in the middle of a long transfer,
- clock skew makes a valid token look invalid (a token that is "not yet valid" or "already expired" according to the other side's clock),
- the authentication service itself is the throttled resource.
(How access models work is a separate topic. The operational fact that credentials expire during long operations belongs here.)
What protocols forbid
- You cannot know whether a failed request happened.
- You cannot have both caller-chosen response shapes and predictable server cost.
- You cannot get semantic compatibility by copying a wire protocol.
- You cannot push without accepting duplicates.
- You cannot change a widely adopted interface atomically, and you cannot cheaply change one at all.
The escapes and their prices
| Escape | What it buys | What it costs |
|---|---|---|
| Idempotency | Escapes retry ambiguity | A deduplication key and the state to remember it |
| Statelessness | Escapes server affinity | Re-sent context and extra round trips |
| Declared schemas | Escapes run-time type errors | Build-time coupling |
| Unload | Escapes the cursor's pace-matching | Durable storage in the middle, and no interactivity |
| Compatibility layers | Escape integration cost | A semantic gap that the user finds later |
| Published limits | Escape surprise throttling | The limit becomes a promise you cannot quietly change |