Why Deprecation Needs a Strategy
Removing or changing an API endpoint without warning breaks integrations that took clients weeks to build. Enterprise contracts, SLAs, and developer trust all depend on predictable, respectful deprecation. A good deprecation strategy protects both sides: you can evolve your API, and clients get enough runway to migrate.
The core challenge: you cannot force clients to update. You can only communicate clearly, make migration easy, and eventually enforce the sunset after adequate notice.
The Sunset Header (RFC 8594)
RFC 8594 defines the Sunset header — a machine-readable timestamp indicating when a resource will become unavailable:
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
This lets clients programmatically detect deprecation and trigger alerts in their own monitoring pipelines. Any API client worth its salt will log or alert on a Sunset header — especially if it is within a short window.
Deprecation Header (Companion)
The Deprecation header, defined in the same RFC 8594 ecosystem (and separately in IETF draft-ietf-httpapi-deprecation-header), marks the date when deprecation started:
Deprecation: Sat, 01 Mar 2026 00:00:00 GMT
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Link: <https://docs.example.com/api/v3/migration>; rel="deprecation"
The Link header with rel="deprecation" points to your migration guide — clients that follow RFC 8594 can auto-discover the migration documentation.
Implementation Example
# Django middleware to add Sunset headers to deprecated endpoints
from datetime import datetime, timezone
from email.utils import format_datetime
DEPRECATED_ENDPOINTS = {
'/api/v1/': {
'sunset': datetime(2026, 8, 1, tzinfo=timezone.utc),
'deprecation': datetime(2026, 3, 1, tzinfo=timezone.utc),
'migration_url': 'https://docs.example.com/api/v2/migration',
},
}
class DeprecationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
for prefix, info in DEPRECATED_ENDPOINTS.items():
if request.path.startswith(prefix):
response['Sunset'] = format_datetime(info['sunset'], usegmt=True)
response['Deprecation'] = format_datetime(
info['deprecation'], usegmt=True
)
response['Link'] = (
f'<{info["migration_url"]}>; rel="deprecation"'
)
return response
Communication Plan
Headers are machine-readable but humans need more than HTTP headers. A complete communication plan includes:
Announcement Cadence
| Timeline | Action |
|---|---|
| T-6 months | Blog post + changelog entry + email to all users |
| T-3 months | Dashboard banner for active API users |
| T-1 month | Email reminder to users still on deprecated endpoint |
| T-2 weeks | Final warning email; support escalation for enterprise clients |
| T-0 | Sunset enforcement |
Changelog and Documentation
Your migration guide should include:
- Side-by-side old vs new endpoint comparison
- Request/response schema diffs
- Code samples for the most popular SDKs
- A migration checklist developers can track
## Migrating from v1 to v2
### Changed Endpoints
| v1 | v2 |
|----|----|
| GET /api/v1/users | GET /api/v2/users |
| POST /api/v1/users | POST /api/v2/users |
### Response Schema Changes
- `user.name` is now `user.full_name`
- `user.created` (epoch int) is now `user.created_at` (ISO 8601 string)
Migration Tooling
Redirect Proxies
During the deprecation window, serve the old endpoint via a compatibility proxy that translates requests to the new format. This lets clients migrate lazily:
# v1 endpoint proxies to v2 with field translation
@api_view(['GET'])
def users_v1(request):
# Add deprecation headers
response = users_v2(request) # delegate to v2 logic
# Translate v2 response schema back to v1
data = response.data
for user in data:
user['name'] = user.pop('full_name', '')
return Response(data, headers={
'Sunset': 'Sat, 01 Aug 2026 00:00:00 GMT',
})
Compatibility Shims
For SDK-based APIs, publish a compatibility shim that maps old method signatures to new ones and emits deprecation warnings in the client's language:
import warnings
class APIClientV1:
def get_user(self, user_id: int) -> dict:
warnings.warn(
'get_user() is deprecated. Use client.users.get() instead. '
'This method will be removed on 2026-08-01.',
DeprecationWarning,
stacklevel=2,
)
return self._v2_client.users.get(user_id)
Measuring Adoption
Tracking Deprecated Endpoint Usage
# Log every hit to deprecated endpoints for migration tracking
import structlog
logger = structlog.get_logger(__name__)
class DeprecationMiddleware:
def __call__(self, request):
response = self.get_response(request)
if self._is_deprecated(request.path):
logger.warning(
'deprecated_endpoint_hit',
path=request.path,
client_id=request.META.get('HTTP_X_CLIENT_ID'),
days_until_sunset=self._days_remaining(request.path),
)
return response
Build a dashboard showing:
- Daily request volume to deprecated endpoints by client ID
- Migration progress (% reduction since announcement)
- Projected sunset feasibility based on current migration velocity
Automated Sunset Enforcement
On the sunset date, return 410 Gone with a clear error body pointing to the migration guide:
{
"error": "endpoint_sunset",
"message": "This endpoint was retired on 2026-08-01.",
"migration_guide": "https://docs.example.com/api/v2/migration"
}
Use 410 Gone rather than 404 Not Found — it explicitly signals intentional removal rather than a missing resource, and is treated differently by crawlers and client retry logic.