Skip to main content
API Design Mastery

Deprecation as a Process

Ravinder··6 min read
API DesignRESTGraphQLgRPCDeprecation
Share:
Part 10 of 10
Deprecation as a Process

Shipping a new API version is the easy part. Retiring the old one is where most teams fail. You announce the sunset date, a handful of teams migrate, and then nothing happens — until the day before cutover when you discover three critical internal services and two large enterprise customers are still on the old version. Deprecation is not a date on a calendar. It is a process: continuous signaling, per-client measurement, proactive outreach, and a dual-stack operation period that keeps the lights on while everyone migrates.

Why Deprecation Fails

The typical deprecation goes:

  1. Ship v2.
  2. Post a blog post.
  3. Set a sunset date 6 months out.
  4. Forget about it.
  5. Panic 2 weeks before the date.

The failure mode is that clients do not read blog posts. Automated systems never read blog posts. The signal must be in the API response itself, visible to every caller on every request.

The Sunset Header (RFC 8594)

RFC 8594 defines the Sunset header — a machine-readable signal that a resource or API version will become unavailable at a specific time:

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Wed, 01 Apr 2026 00:00:00 GMT
Link: <https://api.example.com/v2/orders/42>; rel="successor-version",
      <https://api.example.com/docs/migration/v1-to-v2>; rel="deprecation"

Fields:

  • Deprecation: true (or a date) — signals that this endpoint is deprecated.
  • Sunset — the RFC 7231 date after which the endpoint will stop responding.
  • Link with rel="successor-version" — the replacement URL.
  • Link with rel="deprecation" — the migration guide URL.

Add these headers from the moment v2 is generally available — not after you set the sunset date. Every response from a deprecated endpoint is an opportunity to prompt migration.

Deprecation Timeline

gantt title API v1 Deprecation Timeline dateFormat YYYY-MM-DD section Preparation v2 development :2025-07-01, 2025-09-30 v2 beta :2025-09-01, 2025-10-01 section Active Dual-Stack v2 GA launch :milestone, 2025-10-01, 0d Deprecation headers on v1 :2025-10-01, 2026-04-01 section Rundown Usage outreach (80% clients) :2026-01-01, 2026-03-01 Usage outreach (remaining) :2026-03-01, 2026-03-31 section Sunset v1 Sunset :milestone, 2026-04-01, 0d v1 returns 410 Gone :2026-04-01, 2026-07-01 v1 fully removed :milestone, 2026-07-01, 0d

A minimum dual-stack window of 6 months for external APIs; 3 months for internal services with known clients. Enterprise contracts may require 12 months — account for this in your SDK and API agreements.

Per-Client Usage Tracking

Deprecation outreach is impossible without data. Track v1 usage per API key:

SELECT
    api_key,
    tenant_id,
    COUNT(*) AS request_count,
    MAX(requested_at) AS last_seen_at
FROM api_access_logs
WHERE api_version = 'v1'
    AND requested_at > NOW() - INTERVAL '30 days'
GROUP BY api_key, tenant_id
ORDER BY request_count DESC;

Pipe this into a weekly report. Sort by request_count DESC to prioritize outreach. The long tail of low-volume callers is usually the hardest to find — they have automated scripts running on cron jobs that no one remembers.

Expose usage data to customers through your dashboard so they can self-serve the answer to "are we still on v1?"

Migration Guides

A migration guide that just lists breaking changes is not useful. A migration guide that gives a diff for every change, explains the reason, and links to working examples is.

Structure:

# Migrating from v1 to v2
 
## Overview
v2 changes X, Y, and Z. Most changes require minimal code modification.
Estimated migration time: 2–4 hours for a typical integration.
 
## Breaking Changes
 
### 1. Orders endpoint response shape
 
The `customer_name` field has been split into `customer.firstName` and `customer.lastName`.
 
**v1 response:**
{
  "id": "ord_123",
  "customer_name": "Jane Smith"
}
 
**v2 response:**
{
  "id": "ord_123",
  "customer": {
    "firstName": "Jane",
    "lastName": "Smith"
  }
}
 
**Migration:** Update any code that reads `customer_name` to use
`customer.firstName + ' ' + customer.lastName`.

For SDKs, provide a codemod:

npx @example/api-codemod v1-to-v2 ./src

Codemods handle the mechanical changes; migration guides explain the conceptual ones.

Dual-Stack Operation

During the deprecation window, both versions must work. Route at the gateway:

flowchart TD A[API Request] --> B{Version Detection} B -- URL /v1/ or API-Version header < 2025-10-01 --> C[v1 Handler] B -- URL /v2/ or API-Version header >= 2025-10-01 --> D[v2 Handler] C --> E[Add Deprecation Headers] E --> F[Return v1 Response] D --> G[Return v2 Response]

Do not run dual-stack in application code. Keep version routing at the gateway or load balancer. This lets you decommission v1 with a config change rather than a code deploy.

The Final Cutover

On sunset day, v1 endpoints should return 410 Gone (not 404, not 503):

HTTP/1.1 410 Gone
Content-Type: application/problem+json
 
{
  "type": "https://api.example.com/problems/version-sunset",
  "title": "API Version Retired",
  "status": 410,
  "detail": "API v1 was retired on 2026-04-01. Please migrate to v2.",
  "migrationGuide": "https://api.example.com/docs/migration/v1-to-v2",
  "v2BaseUrl": "https://api.example.com/v2"
}

Keep the 410 response active for 3 months after sunset. Do not immediately remove the route — clients that missed the sunset will get a clear, actionable error rather than a cryptic DNS failure.

Automating Deprecation Signals

Build deprecation signaling into your API framework, not your handlers:

# Framework middleware — pseudocode
class DeprecationMiddleware:
    def __init__(self, app, deprecated_versions: dict):
        # {"v1": {"sunset": "2026-04-01", "successor": "/v2"}}
        self.deprecated = deprecated_versions
 
    def __call__(self, request, response):
        version = extract_version(request)
        if version in self.deprecated:
            info = self.deprecated[version]
            response.headers["Deprecation"] = "true"
            response.headers["Sunset"] = info["sunset"]
            response.headers["Link"] = (
                f'<{info["successor"]}>; rel="successor-version"'
            )
        return response

Register deprecated versions in configuration, not code. When you add a new version, the middleware automatically applies deprecation headers to all old versions.

Key Takeaways

  • Send Deprecation and Sunset headers from the day v2 launches — not from the day you set the sunset date. Every response is a migration prompt.
  • Track v1 usage per API key and tenant in your access logs; generate weekly reports sorted by call volume; reach out proactively to the top consumers.
  • Maintain a 6-month minimum dual-stack window for external APIs; enforce routing at the gateway layer so decommissioning is a config change, not a deploy.
  • Write migration guides as diffs, not change lists — show exactly what code to change, with before and after examples for every breaking change.
  • Return 410 Gone (not 404 or 503) after sunset, with a body pointing to the migration guide, and keep that 410 active for at least 3 months.
  • Automate deprecation header injection in framework middleware keyed to a version registry so you never have to touch individual handlers to signal a sunset.
Part 10 of 10
Share: