API Deprecation Strategy: Sunset Headers, Migration Guides & Version Retirement
API Deprecation Strategy#
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 it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
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 componentsBuild this architecture
Generate an interactive architecture for API Deprecation Strategy in seconds.
Try it in Codelit →
Comments