API Versioning at CDN Edge: URL, Header, and Content Negotiation Patterns

· James Thornton · 10 min read

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.

CDN Edge Version Routing Architecture API Clients Mobile /v1 Web App /v2 Partner /v3 CDN Edge Router /v1/* → backend-v1 /v2/* → backend-v2 /v3/* → backend-v3 (canary) Edge Cache (per-version) Backend v1 (Legacy) Monthly deploys, PHP 8.x Backend v2 (Current) Weekly deploys, Go 1.23 Backend v3 (Canary) Daily deploys, Rust, 5% traffic
Figure 1: CDN edge routing different API versions to independent backend pools

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.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.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.

StrategyCache Key ComponentsCache FragmentationConfig Complexity
URL pathURL (naturally includes version)None (ideal)None
Query parameterURL + query stringLow (if only version param varies)Low
Custom headerURL + Vary: Api-VersionMedium (N versions × M URLs)Medium
Content negotiationURL + 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.

External References

References & Standards: OWASP CDN Security · W3C Web Performance