API Design · Lesson 7

API Deprecation Headers Explained

Use RFC 9745 Deprecation, deprecation links, and Sunset headers to communicate API lifecycle dates, migration guidance, and removal plans.

API deprecation fails when it exists only in a release note that client teams never read.

RFC 9745 adds a standard HTTP response header that makes lifecycle information visible at runtime. Combined with a migration link, usage monitoring, and an optional Sunset date, it gives clients an actionable path away from an old resource.

Quick answer

Use Deprecation to state when the resource will be or was deprecated:

Deprecation: @1782864000

The value is an RFC 9651 Structured Field Date: an at sign followed by Unix seconds.

Link to a migration guide:

Link: <https://developer.example.com/migrations/orders-v2>;
      rel="deprecation"; type="text/html"

If the resource is expected to stop responding, add Sunset:

Sunset: Thu, 31 Dec 2026 23:59:59 GMT

Deprecation does not remove the endpoint or change its behavior. It is a lifecycle hint. Sunset communicates expected unavailability, but the real policy still belongs in documentation and client communication.

The Sunset date must not be earlier than the Deprecation date.

What RFC 9745 adds

RFC 9745 became a Standards Track RFC in March 2025. It defines:

  • The Deprecation HTTP response header.
  • A registered deprecation link relation.
  • How those signals relate to the existing Sunset header.
  • Scope, behavior, and security guidance.

Before RFC 9745, APIs used inconsistent warnings, custom headers, or response-body metadata. A client could not reliably discover lifecycle state from an ordinary response.

The new header is intentionally small. It communicates one date for the resource represented by the response. It does not define a complete API version policy, replacement endpoint, support agreement, or removal workflow. Those details remain provider responsibilities.

Read API Versioning Explained for choosing compatible evolution, URL versions, or date-based contracts.

Deprecation is a Structured Field Date

The syntax is not a boolean and not an HTTP-date string.

Correct:

Deprecation: @1688169599

This represents Friday, June 30, 2023 at 23:59:59 UTC.

Incorrect forms include:

Deprecation: true
Deprecation: Wed, 31 Dec 2026 23:59:59 GMT
Deprecation: 2026-12-31

RFC 9745 defines Deprecation as an Item Structured Header Field whose value must be a Date under RFC 9651. A Structured Field Date uses an integer number of seconds since the Unix epoch prefixed with an at sign.

The date can be:

  • In the future: the resource will become deprecated then.
  • In the past: the resource became deprecated then.
  • Equal to the current rollout time: deprecation is effective now.

Use UTC and generate the value from one source-of-truth lifecycle configuration. Avoid copying epoch values by hand into several services.

Deprecation is not removal

Deprecating a resource means discouraging new dependencies and asking existing consumers to migrate.

It does not mean:

  • The route now returns an error.
  • The response schema changes.
  • Performance can be intentionally broken.
  • The authentication contract disappears.
  • Existing clients can be terminated without notice.

RFC 9745 says deprecation does not change the behavior of the resource. That separation is useful operationally: clients can observe the warning while their current requests continue to work.

Removal is a later policy action. If an API plans to make the resource unavailable at a specific time, Sunset provides that signal.

A sensible timeline is:

  1. Publish migration documentation.
  2. Add a deprecation policy link before deprecation if useful.
  3. Announce the future Deprecation date.
  4. Measure remaining traffic and contact owners.
  5. Reach the deprecation date without breaking behavior.
  6. Optionally publish a later Sunset date.
  7. Remove only after policy, monitoring, and exceptions allow it.

A date without instructions creates urgency but not progress.

Use the Link response header with rel=“deprecation”:

Link: <https://developer.example.com/migrations/orders-v2>;
      rel="deprecation"; type="text/html"

The link can exist even before the resource is formally deprecated. In that case it can explain the provider’s lifecycle policy and the warning period clients can expect.

Once deprecation is announced, the linked guide should answer:

  • Which resource or version is affected.
  • The exact deprecation and expected removal dates.
  • The supported replacement.
  • Request and response differences.
  • Authentication or scope changes.
  • SDK versions that support the replacement.
  • Rollback or coexistence options.
  • Test environment availability.
  • Contact and exception process.
  • A changelog with date corrections.

Use HTTPS and keep the URL stable. Clients and developer portals may store it. Do not point at a temporary ticket or document that requires an employee-only login for public API consumers.

A response can combine both signals:

Deprecation: @1782864000
Link: <https://developer.example.com/migrations/orders-v2>;
      rel="deprecation"; type="text/html"

Add Sunset only when unavailability is planned

RFC 8594 defines Sunset. It indicates that a resource is expected to become unresponsive after a date.

Unlike Deprecation, Sunset uses the traditional HTTP-date format:

Deprecation: @1782864000
Sunset: Thu, 31 Dec 2026 23:59:59 GMT

This difference is easy to misconfigure:

HeaderDate formatMeaning
DeprecationStructured Field Date, such as @1782864000Deprecated now, in the past, or in the future
SunsetHTTP-dateExpected to become unavailable

Sunset must not be earlier than Deprecation. Validate that invariant in configuration and tests.

Do not add a Sunset date merely to pressure clients when the organization has not approved removal. If the date changes repeatedly, clients stop trusting lifecycle signals.

After sunset, decide the endpoint behavior explicitly. Options include 404, 410, a documented redirect for safe GET resources, or an API-specific problem response. Avoid silently routing write requests to a semantically different endpoint.

Read REST API Status Codes Explained before choosing end-of-life responses.

Understand header scope

By default, Deprecation applies to the resource identified by the response.

If it appears on:

GET /v1/orders/123

a generic client can safely conclude that this resource is deprecated. It cannot automatically conclude that every /v1 route or the entire API is deprecated.

A provider may document a broader scope, for example by placing the header on an API home document and stating that it applies to all v1 resources. Unaware clients will still interpret the standard scope as the one response resource.

For broad API version deprecation, combine several channels:

  • Emit headers on affected routes.
  • Update the API home and version documentation.
  • Publish a migration guide.
  • Notify registered client owners.
  • Update SDK warnings.
  • Add dashboard and usage reports.
  • Track calls by route, account, API key, and SDK version.

Headers are discoverable hints, not a substitute for a client inventory.

Spring Boot implementation

Keep lifecycle dates in typed configuration:

@ConfigurationProperties(prefix = "api.orders-v1")
public record OrdersV1Lifecycle(
        Instant deprecationAt,
        Instant sunsetAt,
        URI migrationGuide
) {
    public OrdersV1Lifecycle {
        if (sunsetAt != null && sunsetAt.isBefore(deprecationAt)) {
            throw new IllegalArgumentException(
                    "sunsetAt must not be before deprecationAt"
            );
        }
    }
}

A filter can apply the headers only to affected routes:

@Component
public final class OrdersV1DeprecationFilter extends OncePerRequestFilter {
    private static final DateTimeFormatter HTTP_DATE =
            DateTimeFormatter.RFC_1123_DATE_TIME
                    .withZone(ZoneOffset.UTC);

    private final OrdersV1Lifecycle lifecycle;

    public OrdersV1DeprecationFilter(OrdersV1Lifecycle lifecycle) {
        this.lifecycle = lifecycle;
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return !request.getRequestURI().startsWith("/v1/orders");
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws ServletException, IOException {
        response.setHeader(
                "Deprecation",
                "@" + lifecycle.deprecationAt().getEpochSecond()
        );
        response.addHeader(
                "Link",
                "<" + lifecycle.migrationGuide() +
                ">; rel=\"deprecation\"; type=\"text/html\""
        );

        if (lifecycle.sunsetAt() != null) {
            response.setHeader(
                    "Sunset",
                    HTTP_DATE.format(lifecycle.sunsetAt())
            );
        }

        chain.doFilter(request, response);
    }
}

Use addHeader for Link because a response may already contain other Link relations. Replacing the header could delete pagination, documentation, or preload links.

If a gateway emits lifecycle headers, do not also generate conflicting dates in the application. Choose one authoritative layer and test the final response after all proxies.

Node.js and Express implementation

Node.js can generate the Structured Field Date directly from milliseconds:

function deprecationHeaders({
  deprecationAt,
  sunsetAt,
  migrationGuide
}) {
  const deprecationSeconds = Math.floor(
    deprecationAt.getTime() / 1000
  );

  if (sunsetAt && sunsetAt < deprecationAt) {
    throw new Error("sunsetAt must not be before deprecationAt");
  }

  return function applyLifecycleHeaders(req, res, next) {
    res.set("Deprecation", "@" + deprecationSeconds);
    res.append(
      "Link",
      "<" + migrationGuide +
        ">; rel=\"deprecation\"; type=\"text/html\""
    );

    if (sunsetAt) {
      res.set("Sunset", sunsetAt.toUTCString());
    }

    next();
  };
}

app.use(
  "/v1/orders",
  deprecationHeaders({
    deprecationAt: new Date("2026-07-31T00:00:00Z"),
    sunsetAt: new Date("2026-12-31T23:59:59Z"),
    migrationGuide:
      "https://developer.example.com/migrations/orders-v2"
  })
);

JavaScript Date can represent invalid input, so production configuration should check Number.isNaN(date.getTime()) before creating middleware.

Use res.append for Link to preserve other relations. Test the value that reaches the client behind the real proxy because some infrastructure merges or rewrites repeated headers.

Client behavior

A client should parse Deprecation as a hint and report it to developers or operations. It should not stop a working request solely because the date has passed.

Useful client actions include:

  • Emit a rate-limited structured log.
  • Record the affected URL and date.
  • Surface the migration link in developer diagnostics.
  • Create an alert before a future deprecation date.
  • Track the first and last observation.
  • Avoid logging the warning on every high-volume request.
  • Continue normal request processing.
  • Escalate when Sunset is close.

A Node.js diagnostic helper might be:

function inspectDeprecation(response) {
  const value = response.headers.get("deprecation");
  if (!value || !value.startsWith("@")) return null;

  const seconds = Number(value.slice(1));
  if (!Number.isSafeInteger(seconds)) return null;

  return {
    deprecationAt: new Date(seconds * 1000),
    sunset: response.headers.get("sunset"),
    link: response.headers.get("link")
  };
}

A complete Link parser is more complex than searching for a substring. Use a standards-aware parser if automation depends on relation types.

Clients must still consult authenticated documentation. A malicious or misconfigured intermediary could inject a header or unsafe link.

Browser and CORS considerations

Browser JavaScript cannot read these cross-origin response headers unless the API exposes them:

Access-Control-Expose-Headers: Deprecation, Sunset, Link

Do this only when browser clients need lifecycle diagnostics. A public developer console or SDK may benefit, while normal product UI often should not display raw deprecation messages to end users.

Read CORS Configuration Best Practices and CORS Preflight Explained before widening exposed headers.

The migration guide may need to be usable by developers without application credentials, but it should not reveal confidential roadmap information. Separate public and private API documentation deliberately.

Monitoring migration progress

A deprecation is complete when consumers migrate, not when a header ships.

Measure:

  • Calls to deprecated routes over time.
  • Unique API keys, accounts, or service identities.
  • SDK and client versions.
  • High-volume and business-critical consumers.
  • Error rate on the replacement.
  • Migration guide traffic.
  • Exceptions and approved extensions.
  • Calls after deprecation and near sunset.

Create an owner map. Anonymous traffic may require gateway logs, customer outreach, or a longer safety margin.

Do not remove a route because total traffic is low if one remaining caller performs payroll, billing, or incident recovery. Weight usage by criticality as well as volume.

Use Audit Logs Explained for Backend Systems and Service Accounts Explained to improve client attribution.

Testing the lifecycle contract

Add automated tests for:

  • Deprecation uses an at sign and integer epoch seconds.
  • The date matches the approved lifecycle configuration.
  • The deprecation Link uses rel=“deprecation”.
  • Existing Link values are preserved.
  • Sunset uses a valid HTTP date.
  • Sunset is equal to or later than Deprecation.
  • Headers appear only on affected resources.
  • Successful and error responses both carry the intended signal.
  • Gateway and application layers do not conflict.
  • Browser clients can read headers only when CORS allows it.
  • Migration documentation returns successfully.
  • No response behavior changes when headers are enabled.

Also run a production-like smoke test through the CDN or API gateway. Unit tests cannot reveal header stripping or rewriting by intermediaries.

Common mistakes

The first mistake is sending Deprecation: true. RFC 9745 requires a Structured Field Date.

The second mistake is formatting Deprecation as an HTTP date. That is the Sunset format.

The third mistake is setting Sunset earlier than Deprecation.

The fourth mistake is announcing a date without a stable migration guide.

The fifth mistake is assuming one header on an API home page automatically applies to every endpoint.

The sixth mistake is overwriting existing Link headers.

The seventh mistake is removing the resource at the deprecation date even though deprecation alone does not mean unavailability.

The eighth mistake is emitting warnings without monitoring which clients remain.

Practical rollout checklist

Before emitting the headers:

  • Approve the replacement contract and migration guide.
  • Validate the dates in configuration.
  • Decide the exact resource scope.
  • Inventory known consumers and owners.
  • Add metrics by client and route.
  • Test response headers through the gateway.
  • Announce through normal support channels.

During migration:

  • Keep existing behavior stable.
  • Measure adoption weekly.
  • Contact critical consumers directly.
  • Fix replacement gaps before enforcing a deadline.
  • Publish date changes in one authoritative place.

Before sunset:

  • Confirm remaining usage and exceptions.
  • Test the chosen end-of-life response.
  • Update SDKs and examples.
  • Keep incident rollback available.
  • Communicate the final change again.

Read API Versioning Explained for compatibility strategy, REST API Design Checklist for contract review, and API Gateway Explained for centralized header emission.

For adjacent standards, read Problem Details for HTTP APIs and API Rate Limit Headers Explained. Review RFC 9745, RFC 8594, and RFC 9651 before implementing lifecycle headers.

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Which description best matches API Deprecation Headers Explained?

2. Which statement matches the lesson's quick answer?