Software Architecture · API Design

Deprecation Windows and the Gates That Must Close Before Removal

How to announce an end date, measure who still depends on an interface, and sequence soft warnings, hard failures, and deletion so that removal is boring.

Removing an endpoint is the only API change that is guaranteed to break every remaining caller. Every other breaking change breaks a subset defined by how they consume the interface; deletion breaks all of them at once, on a date. That asymmetry explains why deprecation is treated as a separate discipline with its own protocol, and why the protocol is mostly about discovering dependencies that nobody documented.

AIP-180 states the compatibility premise that deprecation eventually withdraws: existing client code must not be broken by a new minor or patch release, and old clients must work against newer servers within the same major version.

The mechanical part is straightforward. RFC 8594 defines the Sunset response header, whose value is an HTTP-date indicating when the resource is expected to become unresponsive. The Deprecation header, defined in RFC 9745, carries the deprecation date as a Structured Fields date. Those two headers, plus a Link header with the deprecation link relation pointing at migration documentation, form the announcement channel for HTTP APIs. Everything hard about deprecation is on either side of that channel: deciding the date, and knowing who is still listening.

Announcement is not a control

A header reaches the caller at the moment of a request. It does not reach the person who owns the integration, and it does not reach them at all if the client library discards response headers, as some do. Sunset is a useful machine-readable signal and a weak human one.

The channels that actually produce migration fall into a few types. Direct contact with registered consumers works when the API has a registry, which is a precondition worth building before it is needed. Per-consumer usage reporting works because it can be addressed to a specific account. Release notes work for the minority who read them. Failures work universally, which is why the final stages of a deprecation involve deliberate failure rather than louder announcements.

The design consequence is that an API without consumer identity cannot deprecate gracefully. If all traffic arrives under one shared credential, there is no way to say “three accounts still call this” and no way to contact them. Tenant-scoped credentials are, among other things, a deprecation prerequisite.

Choosing a date from data

A removal date should be derived from evidence, not from optimism. Two quantities matter: how much traffic the interface still receives, and how many distinct consumers produce it. A small number of requests from one consumer is a conversation. A large number from many consumers is a project.

The measurement has to be per-consumer and per-interface, and it should be taken at the start of the window rather than at the end, because the interesting question is the decay curve. When usage is flat for the first half of a window and drops only after a hard failure, the window was too long: it deferred all the work to the deadline without buying any migration.

There is a subtlety in what counts as usage. A consumer that calls a deprecated endpoint nightly has an obvious dependency. A consumer that calls it only in an error path, or only when a particular configuration flag is set, appears in telemetry as almost nothing and still breaks at the worst possible moment. Request logs undercount these; consumer-reported inventory and code search across internal repositories catch some of them, and nothing catches all of them.

The three-stage ladder

Removal in one step maximises the number of callers surprised. The ladder spreads the surprise across stages that are individually survivable.

Stage one, soft deprecation. The interface keeps working exactly as before. It carries Deprecation and Sunset headers, appears in the documentation as deprecated, produces a warning in logs, and is annotated in the API description. OpenAPI 3.1 provides a boolean deprecated flag on operations and parameters for the machine-readable case. No caller can fail because of this stage, so there is no excuse for skipping it.

Stage two, hard deprecation. Requests still succeed but the provider starts charging for them differently, or applies a lower rate limit, or returns an additional warning payload. The purpose is to make the dependency visible inside the consumer’s own monitoring. A rate limit is often the gentlest effective instrument: it is degradative rather than fatal, and it generates alerts on the consumer side, which is where the migration actually has to be scheduled.

Stage three, scheduled failure. The interface returns an error for defined windows, announced in advance, testing whether any caller still depends on it. Then it is removed.

Each stage needs a minimum duration, and the durations should be published. A common shape is a quarter for stage one, a quarter for stage two, then removal, but the number that matters is the ratio to the slowest consumer’s release cycle rather than the absolute duration. Mobile clients set the floor here, for the reasons described in versioning strategies.

Brownouts as evidence

A brownout is a deliberate, temporary outage of the interface being retired, scheduled and announced in advance. It answers the question that telemetry cannot: whether a caller has a fallback path that will silently absorb the traffic, or whether the dependency is real and will surface as a user-visible failure.

The technique’s value comes from its reversibility. A brownout that reveals unexpected traffic can be cancelled within minutes, whereas a removal cannot. Several brownouts at increasing duration, spread across time zones and days of the week, map the real dependency surface far better than a month of request counts, because scheduled jobs, month-end batch processes, and rarely executed administrative flows do not appear in a weekly average.

Brownouts have a cost that is easy to underestimate: they consume the goodwill of the consumers who already migrated. Their errors are indistinguishable from a real outage on the consumer’s dashboards until someone reads the announcement. Limiting brownouts to announced windows, keeping them short, and returning a distinctive error body that names the deprecation are the mitigations.

Errors that explain themselves

Every failure produced during deprecation should tell the caller what to do. That means a stable machine-readable code in the body alongside the appropriate status code, a link to the replacement, and a mention of the sunset date. The reader who encounters this error is often an on-call engineer at an inconvenient hour, and the difference between a self-describing error and a bare status code is the difference between a fix in an hour and an escalation.

The payload carrying that code is worth designing with the same care as any other, since it is read by someone under time pressure; error contracts and problem details covers the shape. 410 Gone is the correct status for a resource that has been removed, and it is heuristically cacheable by default, so an intermediary may keep serving the gone response after the resource returns. If there is any chance of reinstating the endpoint, 404 or a redirect makes a weaker claim about the removal being permanent. Where the replacement is a direct equivalent, 308 Permanent Redirect preserves the method and body, unlike 301, and is often the cheapest migration of all.

What cannot be deprecated

Some parts of an interface have no migration path and therefore cannot be removed on a schedule. A field that appears in signed tokens cannot be dropped without invalidating every outstanding token. A wire format embedded in firmware on devices that are never updated remains supported for the lifetime of those devices. A data field that customers have exported into their own systems may be needed for their compliance even after the API stops returning it.

The honest response is to mark such items as permanently supported rather than to publish a sunset date that will be postponed. A postponed date teaches consumers that dates are negotiable, and every subsequent announcement is discounted. Reputation for keeping removal dates is the only asset that makes the next deprecation cheap.

Closing the gate

Removal is complete when three conditions hold: traffic to the interface is zero across a period long enough to include scheduled and seasonal callers, the code path is deleted rather than disabled behind a flag, and the documentation, API description, and client libraries no longer mention it. Disabling behind a flag that nobody dares remove is a failure mode at this stage; the flag becomes a permanent branch, and the deprecated behaviour remains reachable by anyone who flips it.

Deleting the path also means deleting its tests and its data. A schema column retained only for the removed interface is technical debt with an owner’s name on it, and the same reasoning that applies to the removal schedule applies to the storage: it goes when the interface goes, or it stays forever with a stated reason.

References

  1. RFC 8594: The Sunset HTTP Header Field — IETF
  2. RFC 8288: Web Linking — IETF
  3. RFC 9745: The Deprecation HTTP Response Header Field — IETF
  4. Google API Improvement Proposals, AIP-180: Backwards compatibility — Google
  5. OpenAPI Specification 3.1.0 — OpenAPI Initiative