API Deprecation Strategy: Sunset Headers, Migration Guides & Version Retirement
Removing an API endpoint without warning is the fastest way to destroy developer trust. A proper deprecation strategy gives consumers time to migrate, provides clear guidance, and retires old versions without breaking production systems.
Why Deprecation Strategy Matters#
Without a strategy:
Day 1: Remove /v1/users endpoint
Day 1: 47 partner integrations break
Day 1: Support tickets flood in
Day 2: Emergency rollback
With a strategy:
Month 0: Announce deprecation, add sunset headers
Month 3: Send migration reminders to active consumers
Month 6: Return warnings in response bodies
Month 9: Throttle deprecated endpoint (rate limit)
Month 12: Retire endpoint, return 410 Gone
Month 12: Zero breakage — everyone migrated
Sunset Headers (RFC 8594)#
The Sunset header is an HTTP standard that tells clients when an endpoint will be retired.
Adding Sunset Headers#
HTTP/1.1 200 OK
Content-Type: application/json
Sunset: Sat, 29 Mar 2027 00:00:00 GMT
Deprecation: Sat, 29 Mar 2026 00:00:00 GMT
Link: <https://api.example.com/docs/migration/v1-to-v2>; rel="successor-version"
Link: <https://api.example.com/docs/deprecation/users-v1>; rel="deprecation"
{
"data": [...]
}
Header breakdown:
Sunset— the date when the endpoint will stop workingDeprecation— the date when the endpoint was marked deprecatedLinkwithrel="successor-version"— URL to the replacement endpoint docsLinkwithrel="deprecation"— URL to the deprecation notice
Express.js Middleware#
function deprecationHeaders(sunsetDate, migrationUrl) {
return (req, res, next) => {
res.set('Sunset', new Date(sunsetDate).toUTCString());
res.set('Deprecation', new Date().toUTCString());
res.set('Link', [
`<${migrationUrl}>; rel="deprecation"`,
`<${migrationUrl}>; rel="successor-version"`
].join(', '));
// Optional: add warning header
res.set('Warning', '299 - "This endpoint is deprecated. See migration guide."');
next();
};
}
// Apply to deprecated routes
app.get('/v1/users',
deprecationHeaders('2027-03-29', 'https://api.example.com/docs/migrate-users-v2'),
getUsersV1
);
FastAPI Middleware (Python)#
from datetime import datetime
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class DeprecationMiddleware(BaseHTTPMiddleware):
def __init__(self, app, deprecated_routes: dict):
super().__init__(app)
self.deprecated_routes = deprecated_routes
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
path = request.url.path
if path in self.deprecated_routes:
config = self.deprecated_routes[path]
response.headers["Sunset"] = config["sunset_date"]
response.headers["Deprecation"] = config["deprecated_since"]
response.headers["Link"] = (
f'<{config["migration_url"]}>; rel="successor-version"'
)
return response
# Configuration
deprecated_routes = {
"/v1/users": {
"sunset_date": "Sat, 29 Mar 2027 00:00:00 GMT",
"deprecated_since": "Sat, 29 Mar 2026 00:00:00 GMT",
"migration_url": "https://api.example.com/docs/migrate-users-v2"
}
}
Deprecation Timeline#
A 12-month deprecation window is standard for public APIs. Internal APIs can move faster.
Public API Timeline#
Month 0 — ANNOUNCE
├── Add Sunset + Deprecation headers to all deprecated endpoints
├── Publish deprecation notice in changelog and developer portal
├── Email all registered API consumers
└── Update API documentation to mark endpoints as deprecated
Month 3 — REMIND
├── Send targeted emails to consumers still using deprecated endpoints
├── Add deprecation warnings in response body
└── Publish migration guide with code examples
Month 6 — WARN
├── Return deprecation warning in response body for every call
├── Log all consumers still using deprecated endpoints
├── Reach out directly to high-traffic consumers
└── Offer migration support
Month 9 — THROTTLE
├── Reduce rate limits on deprecated endpoints (50% of normal)
├── Return 429 with Retry-After pointing to new endpoint
└── Final warning emails
Month 12 — RETIRE
├── Return 410 Gone for all requests
├── Response body includes migration URL
├── Keep 410 response active for 6+ months (don't return 404)
└── Archive endpoint code (don't delete — you may need to reference it)
Internal API Timeline (Faster)#
Week 0 — Announce in internal channels, add sunset headers
Week 2 — Identify all consuming services via API gateway logs
Week 4 — Coordinate migration with consuming teams
Week 6 — Throttle deprecated endpoints
Week 8 — Retire endpoint (410 Gone)
Migration Guides#
A deprecation without a clear migration path is just a breaking change with extra steps.
Migration Guide Template#
# Migration: /v1/users to /v2/users
## What's Changing
The /v1/users endpoint is deprecated and will be retired on 2027-03-29.
## Why
- v1 returns nested address objects that cause N+1 query issues
- v2 uses flat response format with pagination cursors
- v2 supports field selection (reduce payload size by 60%)
## Quick Start
Replace:
GET /v1/users?page=2&limit=50
With:
GET /v2/users?cursor=abc123&fields=id,name,email
## Field Mapping
| v1 Field | v2 Field | Notes |
|-------------------|---------------|--------------------------|
| user.id | id | No change |
| user.name | display_name | Renamed |
| user.address.city | city | Flattened from nested |
| user.created_at | created_at | Now ISO 8601 (was Unix) |
| user.type | role | Renamed, same values |
## Breaking Changes
1. Pagination changed from offset to cursor-based
2. Date format changed from Unix timestamp to ISO 8601
3. Nested address object flattened to top-level fields
## Code Examples
[Examples for JavaScript, Python, Go, and cURL]
Automated Migration Validation#
# Provide a validation endpoint that checks if consumers are v2-ready
# GET /v1/users/migration-check
@app.get("/v1/users/migration-check")
async def migration_check(request: Request):
api_key = request.headers.get("Authorization")
consumer = get_consumer(api_key)
return {
"consumer": consumer.name,
"v1_calls_last_30d": consumer.v1_call_count,
"v2_calls_last_30d": consumer.v2_call_count,
"migration_status": "in_progress" if consumer.v2_call_count > 0 else "not_started",
"deprecated_endpoints_used": [
"/v1/users",
"/v1/users/{id}/address"
],
"v2_equivalents": {
"/v1/users": "/v2/users",
"/v1/users/{id}/address": "/v2/users/{id}?fields=city,state,zip"
},
"sunset_date": "2027-03-29T00:00:00Z",
"days_remaining": 365
}
Automated Deprecation Warnings#
Response Body Warnings#
{
"data": [...],
"_deprecation": {
"message": "This endpoint is deprecated and will be removed on 2027-03-29.",
"migration_guide": "https://api.example.com/docs/migrate-users-v2",
"replacement": "GET /v2/users",
"sunset_date": "2027-03-29T00:00:00Z"
}
}
API Gateway Deprecation Policy (Kong)#
plugins:
- name: response-transformer
config:
add:
headers:
- "Sunset: Sat, 29 Mar 2027 00:00:00 GMT"
- "Deprecation: true"
json:
- "_deprecation.message: This endpoint is deprecated."
- "_deprecation.sunset: 2027-03-29"
- "_deprecation.migration: https://docs.example.com/v2"
route: users-v1
Monitoring Deprecated Endpoint Usage#
# Track who is still calling deprecated endpoints
from prometheus_client import Counter
deprecated_calls = Counter(
'api_deprecated_endpoint_calls_total',
'Calls to deprecated endpoints',
['endpoint', 'consumer', 'version']
)
def track_deprecated_usage(endpoint, consumer_id, version):
deprecated_calls.labels(
endpoint=endpoint,
consumer=consumer_id,
version=version
).inc()
# Grafana alert rule
ALERT DeprecatedEndpointHighUsage
IF rate(api_deprecated_endpoint_calls_total[1h]) > 100
LABELS { severity = "warning" }
ANNOTATIONS {
summary = "High usage of deprecated endpoint",
description = "{{ $labels.endpoint }} is still receiving {{ $value }} calls/hour from {{ $labels.consumer }}"
}
Backward Compatibility Period#
What to Guarantee During Deprecation#
SAFE — these changes are backward compatible:
✓ Adding new optional fields to responses
✓ Adding new optional query parameters
✓ Adding new endpoints
✓ Adding new enum values (if clients handle unknown values)
✓ Increasing rate limits
UNSAFE — these changes break backward compatibility:
✗ Removing or renaming response fields
✗ Changing field types (string to integer)
✗ Changing error response format
✗ Reducing rate limits without notice
✗ Requiring new mandatory parameters
✗ Changing authentication mechanism
Compatibility Shim Pattern#
# Keep v1 working by translating v2 responses to v1 format
@app.get("/v1/users")
async def get_users_v1(request: Request):
# Call v2 internally
v2_response = await get_users_v2(request)
# Transform v2 format back to v1 format
v1_users = [
{
"user": {
"id": user["id"],
"name": user["display_name"], # v2 renamed this
"address": { # v2 flattened this
"city": user["city"],
"state": user["state"]
},
"created_at": int( # v2 uses ISO, v1 uses Unix
datetime.fromisoformat(user["created_at"]).timestamp()
)
}
}
for user in v2_response["data"]
]
return {"data": v1_users}
Version Retirement Checklist#
Pre-retirement (Month 11):
□ All high-traffic consumers confirmed migrated
□ Deprecation warnings active for 6+ months
□ Migration guide published and linked in sunset headers
□ Support team briefed on retirement date
□ 410 Gone response prepared with migration URL
Retirement day (Month 12):
□ Deploy 410 Gone response for all deprecated endpoints
□ Verify no 5xx errors from the change
□ Monitor error rates in consuming services
□ Keep API gateway route active (for 410 response)
Post-retirement (Month 12+):
□ Keep 410 response active for 6 months minimum
□ Archive v1 code (do not delete)
□ Remove v1 from API documentation navigation (keep as archived page)
□ Update SDK/client library to remove deprecated methods
□ Write post-mortem on migration success rate
Common Mistakes#
- No sunset headers — clients have no machine-readable way to detect deprecation
- Returning 404 instead of 410 — 404 means "never existed," 410 means "gone permanently" and tells clients to stop retrying
- Deprecating without a migration guide — you are asking consumers to reverse-engineer the replacement
- Too short a timeline for public APIs — external developers need at least 6-12 months
- Not tracking who uses deprecated endpoints — you cannot target migration outreach without usage data
Summary#
| Phase | Action | Timeline |
|---|---|---|
| Announce | Sunset headers, changelog, emails | Month 0 |
| Remind | Targeted emails, response warnings | Month 3 |
| Warn | Body warnings, direct outreach | Month 6 |
| Throttle | Reduced rate limits | Month 9 |
| Retire | 410 Gone with migration URL | Month 12 |
| Archive | Keep 410 active, archive code | Month 12+ |
Plan your API architecture at codelit.io — 415 architecture articles and growing. Generate API diagrams, version strategies, and deprecation timelines for any system.
Try these templates
OpenAI API Request Pipeline
7-stage pipeline from API call to token generation, handling millions of requests per minute.
8 componentsDistributed Rate Limiter
API rate limiting with sliding window, token bucket, and per-user quotas.
7 componentsAPI Gateway Platform
Kong/AWS API Gateway-like platform with routing, auth, rate limiting, transformation, and developer portal.
8 componentsContinue learning
Go deeper on API design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
API Design Patterns
JJ Geewax · 2021
Chapter-length treatment of pagination, long-running operations and versioning, with trade-offs.
Contract Testing in Action
Marie Cruz, Lewis Prescott · 2024
Stands up Pact contract tests between consumer and provider and wires them into a CI pipeline.
4.3 (6)Kindle editionRESTful Web API Patterns and Practices Cookbook
Mike Amundsen · 2022
Recipes for hypermedia, retries and service orchestration you can look up mid-implementation.
3.7 (4)Fundamentals of Software Architecture
Mark Richards, Neal Ford · 2025
Scores eight architecture styles against the same characteristics, so trade-offs are side by side.