REST API Deprecation Done Right: Using the Sunset and Deprecation Headers
Use the Sunset and Deprecation headers to deprecate REST APIs safely—syntax, timelines, code examples, client behavior, and rollout checklists.
Image used for representation purposes only.
Overview
Deprecating a REST API is easy to announce and hard to execute. Done well, it’s a predictable, low‑drama change that gives consumers clear signals and ample time to migrate. The Sunset and Deprecation HTTP response headers are the backbone of that experience: they communicate intent, dates, and next steps in a machine‑ and human‑readable way—without breaking existing responses.
This article explains how these headers work, when to send them, how to wire them into your stack, and how clients should react. It also includes implementation snippets, rollout guidance, and a practical checklist you can adopt today.
What the headers mean
- Deprecation header: Tells clients that the resource, operation, or behavior they just used is deprecated. It may carry a boolean or a date indicating when deprecation began. It’s commonly accompanied by Link headers that point to deprecation notices and migration guides.
- Sunset header: Announces the date and time when the resource is planned to be removed or become non‑functional. Think of it as the “end of service” timestamp.
Both are ordinary response headers; they don’t alter response bodies or status codes and travel through proxies as end‑to‑end metadata.
Why use headers instead of just docs or email?
- They’re delivered exactly when and where they’re relevant—at call time, per endpoint.
- They’re observable and scriptable. SDKs, CLIs, and monitoring can surface them.
- They reduce surprise. Clients see the precise date, not a vague blog post.
- They provide a uniform, automated contract that can be tested and enforced.
Header syntax at a glance
Examples for an endpoint that will be removed on January 31, 2026 at 23:59:59 GMT.
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Sat, 31 Jan 2026 23:59:59 GMT
Link: <https://developer.example.com/changelog#users-v1-deprecation>; rel="deprecation"; type="text/html"
Link: <https://api.example.com/v2/users>; rel="successor-version"
Link: <https://developer.example.com/migrate/users-v1-to-v2>; rel="alternate"; type="text/html"
Notes:
- Sunset uses an HTTP-date in GMT. Don’t use local time.
- Deprecation may be “true” or a date (the date the feature was deprecated). If you include a date, use the same HTTP-date format.
- Link relations commonly used: rel=“deprecation” for the notice, rel=“sunset” for the policy, rel=“successor-version” (or rel=“alternate”) for the new endpoint.
When to send each header
- Deprecation: Send on successful responses (2xx) for every deprecated path, operation, or field. Consider also sending on 3xx/4xx if they still indicate a functioning but deprecated behavior (e.g., soft‑deprecated redirects).
- Sunset: Begin sending as soon as you have a credible removal date. If you don’t know the date yet, omit Sunset and include only Deprecation + Link to the evolving plan; add Sunset once the date is set.
A deprecation lifecycle that works
Below is a pragmatic lifecycle you can adopt today. As a concrete example, assume today is November 4, 2026.
- Announce (T0): Publish the plan and start returning Deprecation: true with migration links.
- Freeze (T0 + 30–60 days): No new features on the deprecated surface; telemetry and outreach focus starts.
- Set the deadline (Sunset date): Communicate a specific HTTP-date (e.g., Sat, 31 Jan 2026 23:59:59 GMT) via Sunset and docs.
- Ramp warnings (last 60–90 days): Increase observability, targeted emails, dashboard banners; SDKs surface countdowns.
- Sunset: Return 410 Gone for removed endpoints, or 404 if appropriate. Provide a problem+json body with links to the successor.
- Post-sunset grace (optional, 2–4 weeks): Return 451/497‑style vendor codes in non‑prod or a limited allowlist if needed, but avoid reopening the surface.
Server implementation patterns
Node.js (Express) middleware
// deprecation.js
const DEPRECATIONS = {
'/v1/users': {
deprecatedSince: 'Mon, 04 Nov 2024 00:00:00 GMT',
sunset: 'Sat, 31 Jan 2026 23:59:59 GMT',
links: [
'<https://developer.example.com/changelog#users-v1-deprecation>; rel="deprecation"; type="text/html"',
'<https://api.example.com/v2/users>; rel="successor-version"',
'<https://developer.example.com/migrate/users-v1-to-v2>; rel="alternate"; type="text/html"'
]
}
};
function deprecationHeaders(req, res, next) {
const match = Object.keys(DEPRECATIONS).find(p => req.path.startsWith(p));
if (match) {
const d = DEPRECATIONS[match];
res.setHeader('Deprecation', d.deprecatedSince || 'true');
if (d.sunset) res.setHeader('Sunset', d.sunset);
d.links.forEach(v => res.append('Link', v));
// CORS: expose headers to browsers
res.append('Access-Control-Expose-Headers', 'Deprecation, Sunset, Link');
}
next();
}
module.exports = deprecationHeaders;
NGINX (or ingress) snippet
location /v1/users {
add_header Deprecation true always;
add_header Sunset "Sat, 31 Jan 2026 23:59:59 GMT" always;
add_header Link '<https://developer.example.com/changelog#users-v1-deprecation>; rel="deprecation"; type="text/html"' always;
add_header Link '<https://api.example.com/v2/users>; rel="successor-version"' always;
add_header Access-Control-Expose-Headers 'Deprecation, Sunset, Link' always;
proxy_pass http://users-v1;
}
Spring Boot filter
@Component
public class DeprecationFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getRequestURI().startsWith("/v1/users")) {
response.addHeader("Deprecation", "Mon, 04 Nov 2024 00:00:00 GMT");
response.addHeader("Sunset", "Sat, 31 Jan 2026 23:59:59 GMT");
response.addHeader("Link", "<https://developer.example.com/changelog#users-v1-deprecation>; rel=\"deprecation\"; type=\"text/html\"");
response.addHeader("Link", "<https://api.example.com/v2/users>; rel=\"successor-version\"");
response.addHeader("Access-Control-Expose-Headers", "Deprecation, Sunset, Link");
}
chain.doFilter(req, res);
}
}
Client behavior: what “good citizens” do
- Log and surface: Emit a structured warning including the endpoint, Deprecation value, and Sunset date.
- Nudge humans: In interactive tools (CLIs, dashboards), show a clear notice and link to the migration guide.
- Plan migrations: Treat the Sunset date as a deadline. If not present, open a task but keep polling the deprecation link.
- Test gates: Add CI checks that fail on usage of deprecated SDK calls or URIs once Sunset is within a defined window (e.g., 30 days).
- Track coverage: Maintain a matrix of “calling code → endpoint version” to ensure no forgotten jobs or webhooks.
Communicating alternatives with Link relations
Helpful patterns:
- rel=“successor-version”: Points to the new endpoint (e.g., /v2/users).
- rel=“deprecation”: Points to a human‑readable notice and changelog.
- rel=“sunset”: Points to the policy or status page for sunsetting.
- rel=“alternate”: Points to a migration guide, SDK method, or code sample.
By including multiple Link headers, you can drive IDE tooltips, SDK warnings, and doc-site integration automatically.
Observability and governance
- Telemetry: Break down traffic by deprecated vs. non‑deprecated endpoint, by account, and by user agent.
- SLOs: Define an SLO for “% of traffic still on deprecated surfaces.” Trigger outreach when thresholds are crossed.
- Targeted outreach: Use call logs to email the right maintainers with sample requests and timelines.
- Change management: Store deprecation metadata (paths, since, sunset, links) in a single registry that feeds gateways, SDKs, and docs.
Handling bodies and status codes on/after sunset
Before sunset:
- Continue returning normal 2xx responses with headers.
- Consider adding a response field like warnings[] for human‑visible UIs; keep it non‑breaking.
At/after sunset:
- Return 410 Gone (preferred for removals) with a machine‑readable problem+json body:
{
"type": "https://developer.example.com/errors/endpoint-gone",
"title": "This API version has been removed",
"status": 410,
"detail": "users v1 was removed on 2026-01-31. Use /v2/users.",
"links": {
"successor-version": "https://api.example.com/v2/users",
"migration": "https://developer.example.com/migrate/users-v1-to-v2"
}
}
- If a true successor exists and migration is trivial, a 308 Permanent Redirect may be acceptable—but only if semantics match exactly.
CORS and browser visibility
Browsers do not expose arbitrary headers to JavaScript by default. If your API is used from browsers, add:
Access-Control-Expose-Headers: Deprecation, Sunset, Link
This allows frontend code to read the headers via fetch/XHR and inform users proactively.
Versioning and scope
- Per‑endpoint: Prefer the most specific scope you can. If only one operation is deprecated, don’t blanket‑mark the whole API.
- Per‑version: If an entire version is being retired (e.g., v1), add the headers at the gateway or ingress for that prefix.
- Per‑field: For field‑level deprecations in JSON responses, you can still send headers; also mark the field as deprecated in your OpenAPI and deprecate in SDK types.
OpenAPI integration
- Mark deprecated operations/fields with deprecated: true.
- Add vendor extensions (e.g., x-sunset: 2026-01-31T23:59:59Z, x-deprecationLink: …) to keep metadata in one place.
- Generate: Use codegen or CI to push those values to gateway configs and SDK annotations.
Pitfalls and how to avoid them
- Moving sunset dates repeatedly: Erodes trust. Publish only credible dates; if you must move, shorten the interval next time and explain why.
- Local time confusion: Always use GMT HTTP‑date format.
- Silent 404s after sunset: Prefer 410 Gone with problem+json and links; it aids automation and support.
- Missing CORS exposure: Frontends won’t see the headers unless exposed.
- Environment drift: Don’t forget staging and sandbox; they should mirror production timelines or clearly label differences.
- Partial proxies: Verify intermediate systems (CDNs, gateways) pass through custom headers.
Rollout checklist
- Decide the scope: endpoint(s), version(s), or fields.
- Set credible dates and an internal freeze plan.
- Update OpenAPI with deprecated: true (+ extensions for dates/links).
- Emit Deprecation headers immediately; add Link to the notice and migration guide.
- Add Sunset once the removal date is fixed.
- Expose headers via CORS if browsers are clients.
- Instrument metrics and alerts by endpoint, account, and user agent.
- Communicate widely: docs, changelog, status page, direct outreach.
- Enforce the deadline: switch to 410 Gone and keep links in the error body.
Frequently asked questions
- Should I include headers on error responses? If the call reached a deprecated surface, yes—visibility beats silence.
- What if I don’t have a successor? Still deprecate and sunset; offer alternatives (guides, exports) via Link headers.
- Do caches respect these headers? They’re informational; they don’t alter cacheability. Control caching with Cache-Control as usual.
- Is a deprecation period required? No, but a 3–12 month window is common for customer‑facing APIs.
Final thoughts
Headers are the lightest‑weight, highest‑leverage tool you have for humane API change. Start by returning Deprecation and Link today, add Sunset when you can commit to a date, and wire the same metadata into docs, SDKs, and alerts. Your consumers—and your future self—will thank you.
Related Posts
Designing Nested Resources in REST APIs: A Practical Guide
Clear patterns and best practices for designing nested resources in REST APIs—when to use them, how deep to nest, and how to keep URLs stable.
Header-Based API Versioning: A Practical Guide
A practical guide to header-based API versioning: patterns, negotiation, caching, examples, and rollout strategies for evolving contracts without breaking clients.
REST API rate-limit headers: from X-RateLimit-* to the IETF standard
A practical guide to REST API rate-limit headers: X-RateLimit-*, the IETF RateLimit draft fields, 429/Retry-After, and how to design and consume them.