How to deprecate an API without losing its developers
6 min read
To deprecate an API is to announce that something still works today and will stop working on a stated date, and then to keep both halves of that promise. Most deprecations fail on the second half: the date slips silently, or it arrives and the callers who never saw the notice find out from an error. A deprecation is finished when every affected caller has either migrated or been told, individually, that they have not.
What is API deprecation?
Deprecation is the period between announcing that an endpoint, field or version is going away and actually removing it. During that period the old behaviour keeps working, the documentation says it is going, and every response carries a machine-readable warning. Removal is the separate, later event, often called the sunset. The two get conflated, and the conflation is where the damage happens: “deprecated” starts meaning “might be gone already”, and callers stop trusting either word.
| Term | Meaning | What callers can rely on |
|---|---|---|
| Deprecated | Announced as going away, still works | Full behaviour until the sunset date |
| Sunset | The date it stops working | Nothing after this date |
| Retired / removed | Gone; requests fail | An error, ideally one that names the replacement |
| Legacy | Undefined. Avoid the word | Nothing, which is the problem |
How long should a deprecation period be?
Long enough for a caller to find out and do the work, measured from when the notice reached them rather than from when you wrote it. Ninety days is the common floor for a public web API. Twelve months is normal for anything embedded in software that end users install, because the fix has to ship through their release process too. Google’s versioning guidance, AIP-185, asks for a reasonable transition period and recommends 180 days even before removing beta functionality, and Kubernetes documents its deprecation policy in release counts rather than months, which is the right unit when your callers upgrade by version.
Pick a period, write it down as policy, and stop deciding it per change. A published policy turns each deprecation from a negotiation into an application of a rule.
Writing down the deprecation policy covers the start of the window; sunsetting an API version covers the separate notice needed at the end of it, once the period actually runs out and the version stops working.
The deprecation timeline
Four dates, announced together on day one. Each is a separate changelog entry when it arrives, so the story is told four times to anyone who only reads the changelog.
- Announce. The entry says what is deprecated, why, what replaces it, and the sunset date. Documentation for the old thing gains a banner that links to the migration. Responses gain the headers described below.
- Remind, halfway. A second entry, and a direct message to every caller still using the old behaviour. This is the step that needs usage data: if you cannot list who is still calling the deprecated endpoint, you cannot do it, and that is worth fixing before the next deprecation.
- Brown out, shortly before the date. Return errors for the old behaviour for a short window, an hour or a day, then restore it. Callers who missed every notice find out now, while there is still time. GitHub used scheduled brownouts before retiring password authentication for the API, and it is the single most effective step in this list.
- Sunset. Remove it. The error that replaces it names the replacement and links the migration guide. Keep the error in place for a long time; a 404 tells a caller nothing.
What should a deprecation notice say?
A deprecation notice says what is going, when it stops, what to use instead, and who is affected. Here is the shape, filled in:
GET /v1/reports/dailyis deprecated and stops working on 1 March 2027. It is replaced byGET /v2/reports?granularity=day, which returns the same data with a stable schema and pagination. Affects the 214 integrations that called the v1 endpoint in the last 30 days; if yours is one, you will also receive this notice by email. Migration guide: [link]. Nothing changes until 1 March 2027. From that date the v1 endpoint returns410 Gonewith a link to this entry.
Every sentence carries something the reader needs. The count of affected integrations tells each reader whether to keep reading. “Nothing changes until” is the sentence that lets the ones who are not affected close the tab. The changelog examples page collects entries from teams that write this shape consistently, and it is worth reading three of them before writing your first.
Which headers should a deprecated endpoint send?
Send Deprecation, Sunset and a Link to the successor, on every response from the deprecated
endpoint, from the day of the announcement. The
Deprecation header carries the date the
deprecation took effect; the Sunset header
carries the date the endpoint stops responding; Link: <url>; rel="successor-version" points at
what to use instead.
HTTP/1.1 200 OK
Deprecation: @1756425600
Sunset: Mon, 01 Mar 2027 00:00:00 GMT
Link: <https://api.example.com/v2/reports>; rel="successor-version"
Link: <https://example.com/changelog/daily-reports>; rel="deprecation"
Most callers will never read the headers themselves. Their value is that a caller’s HTTP client, gateway or monitoring can, which turns your deprecation into an alert on their side rather than a page on yours. SDKs you ship should log a warning when they see one.
Who was told, and how do you know?
This is the step that decides whether the sunset is quiet or a support incident, and it is the one that is hardest to do with a changelog alone. A changelog entry tells everyone who reads the changelog. A deprecation has to reach the specific people whose code is going to fail, and the usual way to find them is the same usage data the halfway reminder needs: the API keys, apps or accounts that called the deprecated behaviour recently.
The loop we run: the entry is drafted from the pull request that adds the deprecation, a human reviews the wording and the date, and once it is published the entry itself is the notification. Anyone whose widget feedback about the problem, or request for the replacement, became a GitHub issue that the pull request closes gets a comment on that issue saying it shipped, with a link to the entry. The feed and widget serve the same entry to everyone else, alongside every other entry in the API changelog. What we do not do is let the deprecation become “shipped” before a person has released it; a notice with the wrong date is worse than no notice.
Whatever your tooling, the question to be able to answer on sunset day is: which callers were still using this last week, and which of them did we tell directly? If the answer is “we posted about it”, the sunset is not ready.
What is the difference between deprecating and versioning?
Versioning is how you keep the old behaviour available while the new one exists; deprecation is how you retire the old one. A new API version without a deprecation policy for the previous one is a commitment to run both forever. A deprecation without versioning is a breaking change with a delay. You need both, and the version is the easier half. GraphQL is the exception worth naming: there is usually no version number to bump at all, and GraphQL schema deprecation covers how a single shared schema retires a field with a directive instead.
FAQ
Should a deprecated endpoint keep working exactly as before? Yes, until the sunset date. The only permitted changes are the added headers and, near the end, a scheduled brownout you announced in advance.
What status code should a retired endpoint return?
410 Gone, with a body and a Link header pointing at the replacement and the changelog entry.
404 says the URL never existed, which is false and unhelpful.
Can a deprecation period be shortened? Only for security. If the old behaviour is exploitable, say so, shorten the period, and tell every affected caller directly rather than relying on the changelog.
Do I need to deprecate a field, or only whole endpoints? Fields, parameters, enum values, defaults and headers all need the same treatment, because each one can break a correct caller. A removed field is the most common deprecation and the most often skipped.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.