Running multiple API versions in production is common and necessary — but it multiplies your monitoring surface. Each active version needs its own health checks, deprecation tracking, and usage visibility. Teams that monitor only their latest version routinely discover that significant traffic still hits deprecated endpoints, often from integrations that weren't updated before the planned cutover.
The API Versioning Monitoring Problem
Most API version strategies create three distinct monitoring challenges:
Active version monitoring — All currently supported versions must pass availability and correctness checks. v1 and v2 may have different response shapes, authentication requirements, and rate limits.
Deprecation tracking — Deprecated versions need usage monitoring to understand when it's safe to sunset them. You can't decommission a version with active traffic without breaking customers.
Version drift detection — Over time, the behavior of older versions can drift from documented contracts as shared infrastructure changes. Monitoring catches these silent regressions.
Versioning Strategies and Their Monitoring Implications
URL Path Versioning
GET /v1/users/{id}
GET /v2/users/{id}
Easiest to monitor — each version has a distinct URL path. Create separate monitors for each active version.
monitors:
- name: "Users API v1"
url: "https://api.example.com/v1/users/health"
interval: 60
- name: "Users API v2"
url: "https://api.example.com/v2/users/health"
interval: 60
- name: "Users API v3 (current)"
url: "https://api.example.com/v3/users/health"
interval: 60
Header-Based Versioning
GET /users/{id}
Accept: application/vnd.example.v2+json
Requires monitors capable of sending custom headers:
monitors:
- name: "Users API v1 (header versioned)"
url: "https://api.example.com/users/health"
headers:
Accept: "application/vnd.example.v1+json"
interval: 60
- name: "Users API v2 (header versioned)"
url: "https://api.example.com/users/health"
headers:
Accept: "application/vnd.example.v2+json"
interval: 60
Query Parameter Versioning
GET /users/{id}?version=2
Similar to URL path versioning in monitoring terms — include the version parameter in each monitor URL.
Building a Version Registry
Track all active API versions and their status centrally:
# api_version_registry.py
from dataclasses import dataclass
from datetime import datetime, date
from enum import Enum
from typing import Optional
class VersionStatus(Enum):
ACTIVE = "active"
DEPRECATED = "deprecated"
SUNSET = "sunset"
@dataclass
class APIVersion:
version: str
status: VersionStatus
released_date: date
deprecated_date: Optional[date]
sunset_date: Optional[date]
base_url: str
health_endpoint: str
documentation_url: str
breaking_changes_from_previous: list[str]
# Version registry
API_VERSIONS = [
APIVersion(
version="v1",
status=VersionStatus.DEPRECATED,
released_date=date(2023, 1, 15),
deprecated_date=date(2024, 6, 1),
sunset_date=date(2025, 6, 1),
base_url="https://api.example.com/v1",
health_endpoint="https://api.example.com/v1/health",
documentation_url="https://docs.example.com/api/v1",
breaking_changes_from_previous=[]
),
APIVersion(
version="v2",
status=VersionStatus.DEPRECATED,
released_date=date(2024, 1, 10),
deprecated_date=date(2025, 1, 1),
sunset_date=date(2025, 12, 31),
base_url="https://api.example.com/v2",
health_endpoint="https://api.example.com/v2/health",
documentation_url="https://docs.example.com/api/v2",
breaking_changes_from_previous=[
"Renamed 'user_id' field to 'id' in all responses",
"Removed legacy 'format' query parameter",
"Changed pagination from offset to cursor-based"
]
),
APIVersion(
version="v3",
status=VersionStatus.ACTIVE,
released_date=date(2025, 1, 15),
deprecated_date=None,
sunset_date=None,
base_url="https://api.example.com/v3",
health_endpoint="https://api.example.com/v3/health",
documentation_url="https://docs.example.com/api/v3",
breaking_changes_from_previous=[
"Authentication now requires Bearer tokens (removed Basic auth)",
"Rate limits changed from per-IP to per-API-key"
]
)
]
def get_active_versions():
return [v for v in API_VERSIONS if v.status == VersionStatus.ACTIVE]
def get_deprecated_versions():
return [v for v in API_VERSIONS if v.status == VersionStatus.DEPRECATED]
def get_versions_approaching_sunset(days_threshold=90):
today = date.today()
result = []
for v in API_VERSIONS:
if v.sunset_date and (v.sunset_date - today).days <= days_threshold:
result.append({
"version": v.version,
"sunset_date": v.sunset_date.isoformat(),
"days_until_sunset": (v.sunset_date - today).days
})
return result
Monitoring Deprecated API Versions
The most important thing to monitor about deprecated versions is usage — you need to know when it's safe to decommission them:
# Usage monitoring for deprecated API versions
class DeprecatedVersionMonitor:
def get_version_traffic_report(self, days=30):
"""
Query access logs to understand traffic on each API version.
"""
report = {}
for version in API_VERSIONS:
traffic = self.query_access_logs(
url_prefix=version.base_url,
days=days
)
report[version.version] = {
"status": version.status.value,
"total_requests": traffic.total_requests,
"unique_api_keys": traffic.unique_api_keys,
"unique_ips": traffic.unique_ips,
"daily_average": traffic.total_requests / days,
"last_request_at": traffic.last_request_timestamp,
"top_consumers": traffic.top_consumers[:10]
}
return report
def check_deprecated_version_usage(self):
"""
Alert when deprecated versions still have significant traffic.
This is critical information for sunset planning.
"""
alerts = []
for version in get_deprecated_versions():
if version.sunset_date is None:
continue
traffic = self.query_access_logs(
url_prefix=version.base_url,
days=7
)
days_until_sunset = (version.sunset_date - date.today()).days
# Alert when sunset is approaching and traffic is still significant
if days_until_sunset <= 90 and traffic.total_requests > 0:
alerts.append({
"version": version.version,
"sunset_date": version.sunset_date.isoformat(),
"days_until_sunset": days_until_sunset,
"weekly_requests": traffic.total_requests,
"active_consumers": traffic.unique_api_keys,
"severity": "critical" if days_until_sunset <= 30 else "warning",
"action": "Contact consumers to migrate before sunset"
})
return alerts
def identify_migration_candidates(self, source_version, target_version):
"""
Find API consumers still on an old version who need to migrate.
"""
old_version = next(v for v in API_VERSIONS if v.version == source_version)
old_traffic = self.query_access_logs(
url_prefix=old_version.base_url,
days=30
)
# Match API keys to customer accounts
consumers = []
for api_key in old_traffic.unique_api_keys:
customer = self.lookup_customer_by_api_key(api_key)
if customer:
consumers.append({
"customer_id": customer.id,
"customer_name": customer.name,
"contact_email": customer.technical_contact,
"weekly_calls": old_traffic.calls_by_key.get(api_key, 0),
"last_call": old_traffic.last_call_by_key.get(api_key)
})
return sorted(consumers, key=lambda c: c["weekly_calls"], reverse=True)
Detecting Breaking Changes Between Versions
Monitor that each version's contract remains stable over time:
import requests
import json
import hashlib
from datetime import datetime
class VersionContractMonitor:
"""
Detect when an API version's behavior changes unexpectedly.
This catches regressions in deprecated versions caused by shared
infrastructure changes.
"""
def __init__(self, snapshots_path="./api_snapshots"):
self.snapshots_path = snapshots_path
def take_response_snapshot(self, version, endpoint, auth_header):
"""Take a snapshot of an API response for baseline comparison."""
response = requests.get(
endpoint,
headers={"Authorization": auth_header}
)
snapshot = {
"version": version,
"endpoint": endpoint,
"timestamp": datetime.utcnow().isoformat(),
"status_code": response.status_code,
"response_headers": dict(response.headers),
"response_body": response.json() if response.headers.get("content-type", "").startswith("application/json") else None,
"response_time_ms": response.elapsed.total_seconds() * 1000
}
# Create fingerprint for comparison
if snapshot["response_body"]:
body_str = json.dumps(snapshot["response_body"], sort_keys=True)
snapshot["body_structure_hash"] = hashlib.md5(
json.dumps(self._extract_structure(snapshot["response_body"]), sort_keys=True).encode()
).hexdigest()
return snapshot
def _extract_structure(self, obj, depth=0):
"""Extract the structure (keys and types) without actual values."""
if depth > 5: # Prevent infinite recursion
return "..."
if isinstance(obj, dict):
return {k: self._extract_structure(v, depth+1) for k, v in obj.items()}
elif isinstance(obj, list):
if obj:
return [self._extract_structure(obj[0], depth+1)]
return []
else:
return type(obj).__name__
def check_for_regression(self, version, endpoint, auth_header, baseline_snapshot):
"""Compare current response against baseline snapshot."""
current = self.take_response_snapshot(version, endpoint, auth_header)
issues = []
# Check status code
if current["status_code"] != baseline_snapshot["status_code"]:
issues.append({
"type": "status_code_changed",
"expected": baseline_snapshot["status_code"],
"actual": current["status_code"],
"severity": "critical"
})
# Check response structure
if (current.get("body_structure_hash") and
baseline_snapshot.get("body_structure_hash") and
current["body_structure_hash"] != baseline_snapshot["body_structure_hash"]):
issues.append({
"type": "response_structure_changed",
"message": "Response body structure differs from baseline",
"severity": "critical"
})
# Check response time degradation
if current["response_time_ms"] > baseline_snapshot["response_time_ms"] * 2:
issues.append({
"type": "performance_regression",
"baseline_ms": baseline_snapshot["response_time_ms"],
"current_ms": current["response_time_ms"],
"severity": "warning"
})
return {
"version": version,
"endpoint": endpoint,
"regression_detected": len(issues) > 0,
"issues": issues,
"checked_at": current["timestamp"]
}
Deprecation Header Monitoring
Well-behaved APIs include deprecation notices in response headers. Monitor these to catch what your own API exposes to consumers:
def check_deprecation_headers(endpoint, version):
"""
Verify deprecated API versions return proper deprecation headers.
RFC 8594 defines the Deprecation header standard.
"""
response = requests.get(endpoint)
checks = {}
# RFC 8594 Deprecation header
if "Deprecation" in response.headers:
checks["has_deprecation_header"] = True
checks["deprecation_value"] = response.headers["Deprecation"]
else:
checks["has_deprecation_header"] = False
checks["missing_header_warning"] = "Deprecated API should include Deprecation header"
# Sunset header (RFC 8594)
if "Sunset" in response.headers:
checks["has_sunset_header"] = True
checks["sunset_value"] = response.headers["Sunset"]
# Link header pointing to successor version
if "Link" in response.headers:
checks["has_link_header"] = True
# Should link to successor API docs
# Custom deprecation warning (common pattern)
if "X-API-Warn" in response.headers or "Warning" in response.headers:
checks["has_warning_header"] = True
return checks
Version Sunset Automation
When it's time to sunset a version, monitor the process:
class VersionSunsetManager:
"""
Orchestrate the safe sunset of deprecated API versions.
"""
def pre_sunset_checklist(self, version):
"""
Run checks before setting a version's sunset date.
"""
traffic = self.get_version_traffic(version, days=30)
checklist = {
"version": version,
"checklist_date": datetime.utcnow().isoformat(),
"items": [
{
"check": "no_active_traffic",
"passed": traffic.total_requests == 0,
"detail": f"{traffic.total_requests} requests in last 30 days"
},
{
"check": "all_consumers_notified",
"passed": self.verify_all_consumers_notified(version),
"detail": "All API key holders sent migration notice"
},
{
"check": "migration_guide_available",
"passed": self.check_migration_docs_exist(version),
"detail": "Migration guide published in documentation"
},
{
"check": "successor_version_healthy",
"passed": self.check_successor_version_health(version),
"detail": "Successor API version passing all health checks"
},
{
"check": "support_team_informed",
"passed": self.check_support_ticket_prepared(version),
"detail": "Support team has sunset FAQ prepared"
}
]
}
checklist["all_passed"] = all(item["passed"] for item in checklist["items"])
return checklist
def monitor_sunset_traffic_decline(self, version, target_date):
"""
Track traffic decline trend toward sunset date.
Alert if traffic isn't declining as expected.
"""
weeks_until_sunset = (target_date - date.today()).days // 7
traffic_now = self.get_version_traffic(version, days=7).total_requests
traffic_30d_ago = self.get_version_traffic_historical(version, days_ago=30, window=7).total_requests
if traffic_30d_ago > 0:
decline_rate = (traffic_30d_ago - traffic_now) / traffic_30d_ago
else:
decline_rate = 1.0
# If traffic isn't declining fast enough to reach zero by sunset
if decline_rate < 0.2 and weeks_until_sunset < 8:
return {
"alert": True,
"message": f"Traffic on {version} not declining fast enough before {target_date}",
"current_weekly_requests": traffic_now,
"decline_rate_last_30d": f"{decline_rate:.1%}",
"weeks_until_sunset": weeks_until_sunset,
"recommended_action": "Proactively reach out to remaining consumers"
}
return {
"alert": False,
"current_weekly_requests": traffic_now,
"decline_rate_last_30d": f"{decline_rate:.1%}",
"weeks_until_sunset": weeks_until_sunset,
"on_track": True
}
Multi-Version Health Dashboard
Build a unified view of all API versions:
def generate_version_dashboard():
"""
Generate a dashboard showing the health of all API versions.
"""
monitor = DeprecatedVersionMonitor()
dashboard = {
"generated_at": datetime.utcnow().isoformat(),
"versions": {}
}
for version in API_VERSIONS:
# Health check
health = check_version_health(version.health_endpoint)
# Traffic stats (last 7 days)
traffic = monitor.query_access_logs(version.base_url, days=7)
version_data = {
"status": version.status.value,
"health": health,
"released": version.released_date.isoformat(),
"traffic_7d": traffic.total_requests,
"active_consumers": traffic.unique_api_keys
}
if version.deprecated_date:
version_data["deprecated"] = version.deprecated_date.isoformat()
if version.sunset_date:
days_until_sunset = (version.sunset_date - date.today()).days
version_data["sunset_date"] = version.sunset_date.isoformat()
version_data["days_until_sunset"] = days_until_sunset
version_data["sunset_urgency"] = (
"critical" if days_until_sunset < 30
else "warning" if days_until_sunset < 90
else "info"
)
dashboard["versions"][version.version] = version_data
# Summary alerts
approaching_sunset = get_versions_approaching_sunset(days_threshold=90)
dashboard["alerts"] = {
"versions_approaching_sunset": approaching_sunset,
"total_active_versions": len(get_active_versions()),
"total_deprecated_versions": len(get_deprecated_versions())
}
return dashboard
Alerting Configuration for Version Lifecycle
# Alert rules for API version management
alert_rules:
# Deprecated version health degradation
- name: "Deprecated API Version Down"
condition: "version_health_check_failed AND version_status == 'deprecated'"
severity: warning
message: "Deprecated {{version}} is down — consumers may still depend on it"
channels: [slack-engineering]
# Sunset approaching with active traffic
- name: "Version Sunset Imminent With Active Traffic"
condition: "days_until_sunset <= 30 AND weekly_requests > 0"
severity: critical
message: "{{version}} sunsets in {{days_until_sunset}} days with {{weekly_requests}} weekly requests"
channels: [slack-engineering, pagerduty, email-devrel]
# Unexpected traffic spike on old version
- name: "Unexpected Old Version Traffic Spike"
condition: "version_status IN ('deprecated', 'sunset') AND request_spike > 200pct"
severity: warning
message: "Unexpected traffic spike on {{version}} — possible consumer regression"
channels: [slack-engineering]
# Breaking change detected in older version
- name: "API Version Regression Detected"
condition: "version_contract_check_failed AND version != 'current'"
severity: critical
message: "{{version}} response structure changed — possible shared infrastructure regression"
channels: [slack-engineering, pagerduty]
Conclusion
API version monitoring requires treating each active version as a first-class production service — because your consumers do. The combination of health monitoring for all active versions, usage tracking for deprecated versions, and regression detection for contract drift gives you the visibility to manage the full API lifecycle without unexpected customer impacts. AzMonitor's flexible monitor configuration — supporting custom headers, authentication, and response assertions — makes it straightforward to set up independent health checks for each API version, with alerts that reflect the different severity levels appropriate for current versus deprecated endpoints.
3 monitors free forever · No credit card needed · Set up in 2 minutes
Start monitoring free →