Sunset Header and Deprecation Response Patterns
You have decided to retire a version and you need consumers to actually notice before the removal date. The signals for this are standardized — the Deprecation header, the RFC 8594 Sunset header, and Link relations that point at documentation — but each has an exact wire format that is easy to get subtly wrong, and getting it wrong means standards-aware clients silently ignore the very warning you meant to broadcast. This page is a precise how-to for implementing those response patterns at the gateway and staging brownouts that convert the abstract sunset date into pressure, without shipping a line of upstream code.
This assumes you have already framed the retirement as a staged process. If you have not, read Deprecation Lifecycle Management first for the announce → deprecate → sunset → decommission model these headers plug into, and the broader Advanced Routing & API Versioning context for how versioning and deprecation are two ends of the same routing decision. The headers below are the deprecate and sunset stages made concrete.
The three signals and their exact syntax
Three response headers carry the deprecation contract. Getting the syntax exactly right is what separates a warning clients honour from one they discard.
Deprecation— value is either the tokentrueor an@-prefixed Unix timestamp of when deprecation began, e.g.Deprecation: @1751760000. Prefertrueunless you have a meaningful effective date to advertise.Sunset— value is an HTTP-date in IMF-fixdate form (RFC 8594), identical to the format ofExpires:Sunset: Wed, 31 Dec 2026 23:59:59 GMT. It must be GMT. An ISO 8601 value is malformed and will be ignored.Link— one entry withrel="deprecation"for the human notice, one withrel="sunset"for the migration guide, each a standard RFC 8288 Web Linking value.
A complete, correct response header set for a deprecated version looks like this:
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: </docs/deprecations/orders-v1>; rel="deprecation"; type="text/html"
Link: </docs/migrations/orders-v2>; rel="sunset"; type="text/html"
Warning: 299 - "Deprecated API; migrate to /v2 before the Sunset date"
The Warning: 299 line is a belt-and-braces addition for older clients and intermediaries that log warnings but do not yet understand Deprecation.
Step-by-step: Kong 3.x
Inject the signals with a response-transformer plugin scoped to the retiring route, so every response — success or error — carries them:
# Kong 3.x declarative — deprecate & sunset the v1 route
_format_version: "3.0"
services:
- name: orders-api
url: http://orders.internal:8080
routes:
- name: orders-v1
paths: ["/v1/orders"]
strip_path: true
plugins:
- name: response-transformer
config:
add: # append; does not clobber upstream headers
headers:
- "Deprecation: true"
- "Sunset: Wed, 31 Dec 2026 23:59:59 GMT"
- 'Link: </docs/deprecations/orders-v1>; rel="deprecation"'
- 'Link: </docs/migrations/orders-v2>; rel="sunset"'
- 'Warning: 299 - "Deprecated API; migrate to /v2 before Sunset"'
Because the plugin runs in the response phase, the headers are attached after the upstream replies and appear on gateway-generated errors too. When the version reaches decommission, swap in request-termination to serve a terminal 410 while keeping the response-transformer so the tombstone still advertises the migration link:
# Kong 3.x — terminal decommission response
plugins:
- name: request-termination
route: orders-v1
config:
status_code: 410
content_type: "application/problem+json"
body: '{"title":"API v1 decommissioned","detail":"Migrate to /v2/orders."}'
Step-by-step: Envoy 1.32+
Envoy applies the same headers declaratively with response_headers_to_add on the route, and stages brownouts with the native HTTP fault filter — no plugin hop, no upstream change:
# Envoy 1.32+ — deprecation headers + scheduled brownout on v1
route_config:
name: orders_route
virtual_hosts:
- name: orders_v1
domains: ["api.example.com"]
routes:
- match: { prefix: "/v1/orders" }
route: { cluster: orders_v1_cluster, timeout: 2s }
response_headers_to_add:
- header: { key: "Deprecation", value: "true" }
append_action: APPEND_IF_EXISTS_OR_ADD
- header: { key: "Sunset", value: "Wed, 31 Dec 2026 23:59:59 GMT" }
append_action: OVERWRITE_IF_EXISTS_OR_ADD # single authoritative date
- header:
key: "Link"
value: '</docs/migrations/orders-v2>; rel="sunset"'
append_action: APPEND_IF_EXISTS_OR_ADD # preserve other rels
To stage a brownout, add the fault filter to the same listener and scope it by the version header so only v1 traffic is affected. Ramp numerator across successive windows (10 → 50 → 100) to shed traffic gradually rather than in a single cliff:
# Envoy 1.32+ — brownout: abort a growing % of v1 traffic during announced windows
http_filters:
- name: envoy.filters.http.fault
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault
abort:
http_status: 503
percentage: { numerator: 50, denominator: HUNDRED }
headers:
- name: "x-api-version"
string_match: { exact: "v1" }
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Remove the fault filter to end the window. Because both header injection and fault injection are pure gateway config, the whole sequence is driven by your control plane / GitOps with tested rollback.
Decision matrix: which pattern when
| Stage / goal | Pattern | Header / mechanism |
|---|---|---|
| Announce intent, no date yet | Deprecation only + Link rel="deprecation" |
Deprecation: true |
| Commit to a removal date | Add Sunset alongside Deprecation |
Sunset: <IMF-fixdate> + Link rel="sunset" |
| Reach non-standard-aware clients | Add legacy warning | Warning: 299 - "…" |
| Flush out laggards, recoverably | Scheduled brownout | Envoy fault.abort / Kong request-termination (timed) |
| Ramp pressure smoothly | Percentage traffic-shedding | Envoy fault percentage.numerator growing per window |
| Permanent removal | Terminal response, retain route | 410 Gone via request-termination / direct_response |
Gotchas and failure signals
ISO 8601 in Sunset. Sunset: 2026-12-31T23:59:59Z is malformed. Use the IMF-fixdate Wed, 31 Dec 2026 23:59:59 GMT. The failure is silent — the header is present but ignored — so validate with an actual date parser, not by eyeballing.
Headers only on 2xx. If injection happens in upstream code, gateway-generated errors (auth, rate limit, 5xx) lack the signal. Injecting at the gateway response phase fixes this; verify by forcing a 401 and confirming the headers are still present.
Overwriting Link. Using an overwrite action on Link discards any relations the upstream legitimately sets. Append Link; only overwrite the single-valued Sunset.
Unannounced brownouts. A brownout nobody was told about is an outage. Publish exact windows and the affected version on the same channel as the deprecation notice before the first window runs.
404 at decommission. Returning 404 instead of 410 invites consumer-side debugging and retries. 410 Gone states the removal is intentional and permanent.
Validation
-
Sunsetvalue parses as an IMF-fixdate HTTP-date in GMT — not ISO 8601. -
Deprecationheader is present on 2xx, 4xx, and 5xx responses for the route. -
Linkincludes bothrel="deprecation"andrel="sunset", each resolving to a live page. - Header injection is scoped to the retiring route; the successor version is unaffected.
- Brownout windows are pre-announced and revert cleanly by removing the filter/plugin.
- Traffic-shedding ramps as a growing percentage across windows, not a single cliff.
- Decommission returns
410 Gonewith aproblem+jsonbody linking the migration guide.
FAQ
What date format does the Sunset header require?
RFC 8594 requires the Sunset value to be an HTTP-date in IMF-fixdate form, the same format used by Date and Expires — for example Wed, 31 Dec 2026 23:59:59 GMT. It must be in GMT. ISO 8601 values such as 2026-12-31T23:59:59Z are malformed for this header and standards-aware tooling will ignore them, so the date will not be surfaced to consumers.
Do I need both the Deprecation and Sunset headers?
They serve different roles and are usually emitted together during the sunset stage. Deprecation announces that the resource is deprecated and optionally when that took effect; Sunset commits to the date it will stop responding. During the earlier deprecate stage you may emit only Deprecation because no removal date is fixed yet. Once a date exists, add Sunset alongside it. Pair both with Link headers so clients can resolve documentation.
How should the Link header reference deprecation and sunset documentation?
Use two Link entries: one with rel="deprecation" pointing at the human-readable deprecation notice, and one with rel="sunset" pointing at the migration guide, as registered by RFC 8594. Each is a standard Web Linking (RFC 8288) value, for example </docs/migrations/v2>; rel="sunset". Append them rather than overwriting so any link relations the upstream sets are preserved.
Can I stage a brownout without touching the upstream service?
Yes — that is the point of doing it at the gateway. Envoy’s HTTP fault filter can abort or delay a percentage of requests to the deprecated route, and Kong’s request-termination plugin can return a terminal status for a scheduled window, both without any upstream change. This lets you run a short, recoverable failure that flushes out consumers who ignored the headers, then revert by removing the filter or plugin.
Parent: Deprecation Lifecycle Management
Related
- API Versioning Strategies — how the successor version consumers migrate to is introduced.
- Request & Response Transformation — the plugin family that stamps lifecycle headers at the edge.