API Versioning at CDN Edge: URL, Header, and Content Negotiation Patterns
Every API evolves, and every evolution breaks something. API versioning is the discipline of managing that evolution without forcing all consumers to upgrade simultaneously. When a CDN sits between API clients and origin servers, the versioning strategy directly affects cacheability, routing flexibility, and the operational complexity of running multiple API versions in production. The wrong versioning approach can fragment the CDN cache, complicate routing rules, and create invisible dependencies between CDN configuration and application deployment.
This guide compares the four dominant versioning strategies—URL path, query parameter, custom header, and content negotiation—through the lens of CDN edge delivery. Each approach has distinct implications for cache key design, edge function routing, deprecation workflows, and the operational burden of maintaining concurrent versions.
URL Path Versioning: The CDN-Friendly Default
URL path versioning embeds the version directly in the URL: /v1/users, /v2/users, /v3/users. From the CDN's perspective, each version is a completely separate resource. The cache key naturally includes the version because the URL is different. No custom cache key configuration is needed, no Vary headers complicate the caching layer, and the CDN's standard HTTP caching semantics apply without modification.
// URL path versioning — CDN sees distinct resources
GET /v1/users/42 → Cache key: /v1/users/42
GET /v2/users/42 → Cache key: /v2/users/42
GET /v3/users/42 → Cache key: /v3/users/42
// Each version cached independently
// Standard Cache-Control headers apply per version
// No Vary header needed for versioning
The simplicity extends to CDN routing. Edge functions or CDN routing rules can direct different versions to different origin servers or backend pools. This enables a powerful operational pattern: version 1 runs on a legacy backend with its own deployment schedule, version 2 runs on a modernized backend, and version 3 runs on a canary backend receiving limited traffic. The CDN edge orchestrates this without any of the three backends knowing about each other.
The primary criticism of URL versioning is philosophical rather than technical: it changes the resource's identity when only its representation changes. The user at /v1/users/42 and /v2/users/42 is the same person; only the response format differs. REST purists argue this violates the principle that a URL should identify a resource, not a representation. In practice, however, the operational simplicity of URL versioning outweighs this theoretical concern for the majority of API programs.
Header-Based Versioning: Custom Headers and Cache Complexity
Header-based versioning uses a custom HTTP header to specify the API version: Api-Version: 2 or X-API-Version: 2024-01-15. The URL remains stable across versions, preserving resource identity. The same /users/42 endpoint returns different response formats depending on the version header.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
// Header-based versioning
GET /users/42
Api-Version: 1
→ { "name": "Jane Doe", "email": "[email protected]" }
GET /users/42
Api-Version: 2
→ { "full_name": "Jane Doe", "email_address": "[email protected]", "profile": { ... } }
// CDN cache key MUST include the version header
// Without it, v1 response cached and served to v2 clients
// Response needs: Vary: Api-Version
The CDN cache key problem is the central challenge. Since the URL is identical for all versions, the CDN must include the Api-Version header in the cache key to avoid serving a version 1 response to a version 2 client. This requires either adding Vary: Api-Version to the response (which most CDNs respect automatically) or configuring a custom cache key rule that includes the header value.
The Vary header approach works but has a performance cost. Each unique Api-Version value creates a separate cache entry for the same URL. If you have 5 active API versions and 100,000 popular URLs, the CDN now maintains 500,000 cache entries instead of 100,000. The cache requires more storage and more memory for its lookup index, and the effective cache hit ratio drops because each version's traffic is spread across fewer requests.
Content Negotiation: The RESTful Approach
Content negotiation versioning uses the HTTP Accept header with vendor-specific media types: Accept: application/vnd.myapi.v2+json. This follows the HTTP specification's intended mechanism for selecting resource representations—the same mechanism that negotiates between JSON and XML, or between languages. The server returns the appropriate version based on the media type and sets Content-Type to the matching vendor type.
// Content negotiation versioning
GET /users/42
Accept: application/vnd.myapi.v1+json
→ Content-Type: application/vnd.myapi.v1+json
GET /users/42
Accept: application/vnd.myapi.v2+json
→ Content-Type: application/vnd.myapi.v2+json
// CDN configuration required:
// 1. Add Accept to cache key (Vary: Accept)
// 2. Or normalize Accept header at edge to extract version
// 3. Or use edge function to rewrite to URL versioning internally
From a CDN perspective, content negotiation combines the URL stability of header-based versioning with even more cache key complexity. The Accept header is notoriously varied: browsers send long, complex Accept values with quality factors (q= parameters), and different HTTP clients format the same media type with different whitespace, ordering, and parameter syntax. A Vary: Accept response header can fragment the cache severely because the CDN treats application/vnd.myapi.v2+json and application/vnd.myapi.v2+json; charset=utf-8 as different cache entries.
The solution is an edge function that normalizes the Accept header before it reaches the cache layer. The function parses the media type, extracts the version number, and rewrites the request to include a normalized version identifier that the cache can use efficiently.
Edge Function Implementation: Version Router
An edge function that handles all four versioning strategies provides maximum flexibility. The function checks URL path first (highest priority), then query parameter, then custom header, then Accept header, and finally falls back to a default version. This layered approach supports clients using different versioning conventions against the same API.
// Cloudflare Worker: Universal API version router
export default {
async fetch(request) {
const url = new URL(request.url);
let version = null;
let cleanPath = url.pathname;
// Strategy 1: URL path — /v2/users/42
const pathMatch = url.pathname.match(/^\/(v\d+)\/(.*)/);
if (pathMatch) {
version = pathMatch[1];
cleanPath = '/' + pathMatch[2];
}
// Strategy 2: Query parameter — /users/42?version=2
if (!version && url.searchParams.has('version')) {
version = 'v' + url.searchParams.get('version');
url.searchParams.delete('version');
cleanPath = url.pathname;
}
// Strategy 3: Custom header — Api-Version: 2
if (!version) {
const headerVersion = request.headers.get('Api-Version');
if (headerVersion) version = 'v' + headerVersion;
}
// Strategy 4: Content negotiation — Accept: application/vnd.myapi.v2+json
if (!version) {
const accept = request.headers.get('Accept') || '';
const vendorMatch = accept.match(/application\/vnd\.myapi\.(v\d+)\+json/);
if (vendorMatch) version = vendorMatch[1];
}
// Default version
version = version || 'v2';
// Route to version-specific origin
const origins = {
'v1': 'https://api-v1.origin.example.com',
'v2': 'https://api-v2.origin.example.com',
'v3': 'https://api-v3.origin.example.com',
};
const origin = origins[version] || origins['v2'];
const targetUrl = origin + cleanPath + url.search;
// Add version metadata for cache and observability
const modifiedRequest = new Request(targetUrl, request);
modifiedRequest.headers.set('X-Resolved-Version', version);
const response = await fetch(modifiedRequest);
// Add version headers to response
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set('X-Api-Version', version);
modifiedResponse.headers.set('Vary', 'Api-Version, Accept');
return modifiedResponse;
}
};
Cache Key Design for Versioned APIs
The cache key for a versioned API must include enough information to distinguish responses that differ and exclude information that would fragment the cache unnecessarily. The optimal cache key for each strategy differs significantly.
| Strategy | Cache Key Components | Cache Fragmentation | Config Complexity |
|---|---|---|---|
| URL path | URL (naturally includes version) | None (ideal) | None |
| Query parameter | URL + query string | Low (if only version param varies) | Low |
| Custom header | URL + Vary: Api-Version | Medium (N versions × M URLs) | Medium |
| Content negotiation | URL + Vary: Accept (normalized) | High (Accept header varies wildly) | High |
For header-based and content negotiation strategies, the cache key should be constructed at the edge to include only the normalized version identifier, not the raw header value. This prevents minor header formatting differences from creating separate cache entries for the same version and resource.
Version Deprecation and Sunset at the Edge
RFC 8594 defines the Sunset HTTP response header, which indicates when a resource will become unavailable. Injecting this header at the CDN edge is the cleanest way to communicate deprecation without modifying origin code. The edge function adds the Sunset header to all responses from deprecated versions, along with a Link header pointing to the migration guide.
// Edge-injected deprecation headers
// Response from /v1/* endpoints:
Sunset: Sat, 01 Mar 2027 00:00:00 GMT
Deprecation: true
Link: <https://docs.example.com/migration/v1-to-v2>; rel="successor-version"
X-Api-Version: v1 (deprecated, sunset 2027-03-01)
Gradual version migration uses traffic splitting at the CDN edge. The edge function routes a configurable percentage of v1 traffic to the v2 backend, starting at 1% and increasing as confidence builds. Clients that experience errors on v2 are excluded from the traffic split for a cooldown period. This approach validates the v2 backend under real v1 traffic patterns before forcing the migration.
After the sunset date, the edge function returns 410 Gone for the deprecated version with a response body explaining the migration path. The origin server never sees this traffic—the CDN blocks it at the edge. This protects origin resources from serving deprecated versions and provides a clean signal to API consumers that they must migrate.
Date-Based Versioning: The Stripe Model
Some APIs use dates instead of numbers: Api-Version: 2026-01-15. Each date represents a snapshot of the API's behavior, and the server maintains backward compatibility by applying changes only to requests with a version date equal to or after the change was introduced. Stripe, Twilio, and other APIs use this pattern to decouple breaking changes from sequential version numbers.
At the CDN edge, date-based versioning requires the same header-based cache key strategy as numeric versioning. The edge function should normalize date formats to prevent 2026-01-15 and 20260115 from creating separate cache entries. Additionally, the edge can implement "version pinning" by storing each client's default version and injecting the header when clients omit it, based on their API key or authentication token.
Choosing a Strategy: Decision Framework
The choice between versioning strategies should be driven by the CDN's capabilities, the number of concurrent versions you expect to maintain, and your team's operational maturity with edge function deployments.
Choose URL path versioning when CDN caching efficiency is the top priority, when you maintain 2-3 versions simultaneously, and when the operational simplicity of separate URL spaces outweighs the REST purity concern. This is the right default for most teams.
Choose header-based versioning when you need stable URLs for documentation and SDK generation, when your CDN supports custom cache key configuration, and when you have the operational capability to manage edge functions for version routing.
Choose content negotiation when your API is a truly RESTful hypermedia API where resource identity must be preserved across versions, when you already use content negotiation for format selection (JSON/XML/protobuf), and when you can invest in edge-level Accept header normalization.
Choose date-based versioning when you make frequent small breaking changes (more than 4 per year), when you want to decouple API evolution from discrete version numbers, and when your consumers are sophisticated enough to manage date-based version pinning.