The short version

If you run an API that real customers depend on, retiring an old version is one of the riskiest things you’ll do. Pull the plug too early and a paying integration crashes at 2 a.m. Leave it running forever and you end up maintaining five legacy schemas while shipping zero new features. The middle path is what RFC 8594 and RFC 9745 are designed for. You announce the change inside the response itself, using the Deprecation and Sunset HTTP headers, you give clients a long and predictable runway, and you turn off the lights on a date you published months ago. This guide walks through that playbook for a team of one or five — no enterprise change-management board required.

Why small teams need a deliberate deprecation process

A solo founder usually starts with one API version and one backend. That feels simple until a paying customer writes an integration against it, ships a product, and then asks you to ship a breaking change because the response shape is wrong, or the auth flow is outdated, or you simply want to rename a field. Now you have a real problem: keep the old shape forever and your code rots, or change it and someone in production loses money.

Big platforms solve this with full-time API product managers. You don’t have that. What you do have is a way to make deprecation a property of the response, not a property of an email blast that nobody reads. That’s the whole point of the standards-based headers.

The three headers that do most of the work

The modern deprecation story rests on two RFCs, both written by Erik Wilde and a small set of contributors:

  • RFC 9745 — The Deprecation HTTP response header field. Tells the client that this endpoint is deprecated, or will be deprecated at a given moment.
  • RFC 8594 — The Sunset HTTP response header field. Tells the client the exact moment the endpoint will start returning 410 Gone.

In practice, you will pair Deprecation with a Link header that points at your migration guide. The combination is small, free, and fits inside any API gateway or middleware.

Deprecation: announce the change at runtime

RFC 9745 defines the value as an RFC 9651 date, which is essentially Unix time prefixed with @:

Deprecation: @1735689600

That timestamp is the moment deprecation became effective. Before that date the same header can communicate the future moment of deprecation. The benefit is concrete: a developer inspecting a response, or a client SDK logging warnings in non-production environments, can spot the signal without having to read your blog post. RFC 9745 also formalizes a deprecation link relation type so you can link out to a migration guide from inside the header block.

If Unix time feels unfriendly for humans reading network logs, an HTTP-date variant (Deprecation: Wed, 11 Oct 2023 23:59:59 GMT) is sometimes used. The plain boolean (Deprecation: true) is even simpler, but it loses the timeline, which is the whole reason this header exists.

Sunset: publish the kill date

Sunset: Wed, 11 Nov 2026 00:00:00 GMT

Sunset is the actual retirement date. RFC 8594 is clear that it is just a date — it doesn’t have to mean “deprecated forever,” and a server can use it for any short-lived resource — but in practice it has become the canonical “this endpoint will start returning 410 Gone on this date” signal. Pair it with a Link header that uses rel="sunset" and points at your migration document:

Link: <https://docs.example.com/v1-to-v2>; rel="sunset"

Warning: a less formal but still useful channel

The older Warning HTTP header can carry a short human-readable note (for example, code 299 with a deprecation message). It’s not standardized for this purpose the way Deprecation and Sunset are, but it’s a reasonable place to put a sentence that explains why something is going away, especially if your gateway lets you set it cheaply on the way out.

A realistic timeline for a small team

You can’t copy Stripe’s playbook (typically a 12-month deprecation window for public APIs) and you probably don’t need to. What you need is a timeline that is long enough that no reasonable customer is surprised, and short enough that you don’t carry old code forever. A workable shape:

  • Day 0 — Announce. Email every registered API customer. Update your docs. Publish a changelog entry. Start emitting Deprecation and Link headers on the affected routes.
  • Day 0+30 — Quiet period. Old routes still work, no brownouts. You use this time to chase the noisy 10% of customers who consume the most traffic.
  • Day 0+90 — First reminder. Re-email. Optionally add Sunset to the header block with a date roughly six months out.
  • Day 0+180 — Brownout. Pick one or two off-peak windows per week and have your gateway return 429 or 503 for 15 minutes at a time on the legacy routes. This is the most useful step you’ll do, because it forces dormant integrations to fail loudly before the real shutdown. Clients who weren’t watching the headers will now have their dashboards light up.
  • Sunset date — Shut it down. Endpoint returns 410 Gone with a JSON body pointing at the migration guide.

The brownout step is borrowed from chaos engineering. You are deliberately making a small, scheduled, reversible failure to surface the integrations that haven’t migrated yet. For a small team, even one 15-minute brownout a month before shutdown is enough to catch the worst offenders.

How to actually inject the headers

You have a few realistic options, ordered roughly by how much code you write:

  1. API gateway policy. Most managed gateways (Kong, Zuplo, Tyk, Apigee, Cloudflare API Shield) let you attach an outbound policy that adds or overrides response headers for a matched route. This is the path of least resistance. You configure the Deprecation value, the Sunset date, and the migration URL once per route and forget about it.
  2. Reverse proxy with middleware. If you’re behind Nginx, Envoy, or a custom Node/Go service, add a small middleware that decorates responses on the way out. Keep the configuration in one place — version-controlled — so the sunset date can’t drift.
  3. In-handler. The least clean option. You will forget to update the date in some endpoint. Don’t.

Whichever path you choose, put the sunset date in a single config file. When the date arrives, the same config should switch the endpoint to return 410 Gone. One source of truth, one change.

Keeping two versions alive without burning out

The real cost of deprecation isn’t the announcement — it’s the months of running two code paths. A few ways to keep that manageable:

  • Extract the shared schema. If v1 and v2 differ by three fields, write one internal model and render it twice. The renderer is small; the business logic stays single-source.
  • Isolate the legacy version behind a feature flag. When traffic to v1 drops below a threshold you set (say, 5% of calls for two weeks), that’s your signal to schedule the final shutdown.
  • Measure at the gateway. Log the User-Agent and API key on every call to a deprecated route. You can then email the specific customers still calling it. Without that fingerprinting, you’re emailing everyone and hoping.

None of this is exotic. It’s mostly discipline and a single place to look at traffic.

What clients should do (and how to nudge them)

Most small-team APIs are consumed by other developers who, like you, are busy. They will not read your email. Some of them will have automated CI that catches warnings; most will not. You can still raise the odds:

  • Put the deprecation message in the response body for a short window, not just headers. Some scrapers only look at the body.
  • Add a one-line console.warn snippet to your official client SDKs. Maintainers can ship a minor version that prints a warning when a deprecated route is hit.
  • Keep your migration guide ruthlessly short. Two sections: what changed, and a copy-pasteable example of the new request.

A minimal end-to-end example

Imagine you run POST /v1/charges and want to retire it in favor of POST /v2/payments. On the v1 route, for the next six months, your gateway emits:

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1717200000
Sunset: Wed, 11 Nov 2026 00:00:00 GMT
Link: <https://docs.example.com/migrate-v1-charges>; rel="sunset"
Warning: 299 - "POST /v1/charges is deprecated. Use POST /v2/payments."

{ ...payload... }

On the sunset date, the same route returns:

HTTP/1.1 410 Gone
Content-Type: application/json
Link: <https://docs.example.com/migrate-v1-charges>; rel="sunset"

{"error":"endpoint_retired","migration":"https://docs.example.com/migrate-v1-charges"}

That’s the whole contract: a header that warned for six months, a date you published in advance, and a 410 that points at the next step.

FAQ

How long should the deprecation window be? For a public B2B API, twelve months is the common baseline used by large platforms. For an internal API or a small-team product where you know the customer set, three to six months is often realistic. The exact number matters less than picking one and sticking to it.

Do I still need to email customers if I send the headers? Yes. Headers reach the developers whose SDKs or scripts log responses; email reaches the humans who pay you. The two channels cover different audiences.

What if a customer refuses to migrate? Treat the sunset date as a contract with yourself, not a negotiation. If you keep extending it, the deprecation process becomes meaningless. The brownout step exists to give stragglers a final, reversible warning.

Is Deprecation: true enough? It’s enough to signal that the route is deprecated, but it loses the when. Without a date, you can’t run brownouts, you can’t plan the shutdown, and you can’t write automation that compares against today. Use a date.

Where should the migration guide live? Anywhere with a stable URL — your docs site, a Notion page with a custom domain, a GitHub gist you commit to. The point is that the Link header value should not rot.

The one-line takeaway

Put the sunset date in the response, not just in an email. Tell customers twice (once when you start, once near the end), prove you’re serious with one short brownout, and turn off the lights on the date you published.

Sources