diff --git a/src/content/docs/workers/configuration/versions-and-deployments/skew-protection.mdx b/src/content/docs/workers/configuration/versions-and-deployments/skew-protection.mdx new file mode 100644 index 000000000000000..d7d5738b8920551 --- /dev/null +++ b/src/content/docs/workers/configuration/versions-and-deployments/skew-protection.mdx @@ -0,0 +1,337 @@ +--- +pcx_content_type: configuration +title: Skew Protection +head: [] +description: Revert to an older version of your Worker. +--- +import { Render, WranglerConfig, DashButton } from "~/components"; + +Skew Protection ensures that client-side JavaScript code continues to communicate with the same version of your Worker that originally served it, preventing version mismatch errors during deployments. + +Version skew occurs when a user's browser has loaded JavaScript from one version of your application, but subsequent requests (such as API calls or asset fetches) are handled by a different, incompatible version. For example, imagine your newest deployment changes an API endpoint's response format. Users with JavaScript from the old version loaded in their browser would receive responses they cannot parse, leading to application errors. + +Cloudflare's Skew Protection resolves this problem automatically by ensuring all requests from a client session are routed to the same Worker version that served the initial page load. This works seamlessly with gradual deployments, allowing you to safely roll out new versions without breaking active user sessions. + +### How it works +Skew Protection keeps multiple versions of your Worker deployed simultaneously. Your application includes version information in each request, and Cloudflare automatically routes those requests to the matching Worker version. + +When Skew Protection is enabled on a Worker, +- **Automatic version routing**: Requests are automatically routed to specific versions using the `Cloudflare-Workers-Version-Overrides` header or `?dpl=` query parameter +- **Configurable version TTL**: Versions remain routable for a configurable period (default: 2 days) after deployment, ensuring users on older versions can complete their sessions +- **Version cutoff**: Manually mark a specific version as the cutoff to immediately make all earlier versions unroutable, useful for critical security fixes or breaking changes +- **Version control**: Disable routing to individual faulty versions without affecting other versions in the TTL window + +#### Framework support +If you're using one of the following frameworks with their official Cloudflare adapters, Skew Protection will be automatically enabled by default during the build process: +- SvelteKit +- Remix +- ... + +These adapters automatically configure Skew Protection and implement the routing logic. If you'd like to disable skew protection, you can do so. + +#### Version Identification + +Cloudflare identifies which version to route requests to using the following mechanisms, in priority order: + +1. **Query parameter**: `?dpl=` (highest priority) + - Best for static assets (images, CSS, JavaScript files), iframes, WebSockets, or any request where custom headers cannot be set + - Automatically added by framework adapters to dynamic imports and asset URLs + - Example: `` + - Example: `` + +2. **HTTP header**: `Cloudflare-Workers-Version-Overrides: =""` (fallback) + - Best for API calls, fetch requests, or any request where you can control the HTTP headers + - Uses [Dictionary Structured Header](https://www.rfc-editor.org/rfc/rfc8941#name-dictionaries) format to support multiple Workers in service bindings + - Automatically added by framework adapters to all fetch() calls + - Example: `fetch('/api/data', { headers: { 'Cloudflare-Workers-Version-Overrides': 'my-worker="v870e20d"' } })` + - Example for multiple workers: `Cloudflare-Workers-Version-Overrides: worker-a="v123", worker-b="v456"` + +Query parameters take priority over headers when both are present. This ensures maximum compatibility since query parameters work universally for all request types (including static assets loaded by the browser via HTML tags), while headers only work for programmatic requests where you control the fetch call. + +:::note +Cloudflare's Skew Protection does not use cookies for version tracking. Cookies break multi-tab usage: if a user opens your site in two tabs and refreshes one tab to get a new version, the cookie forces the other tab to also switch versions, causing errors. Query parameters and headers avoid this problem by letting each tab maintain its own version independently. +::: + +#### Request routing + +When a request includes version routing information: + +1. **Header-based routing**: `Cloudflare-Workers-Version-Overrides: my-worker="v870e20d"` +2. **Query parameter routing** (for static assets): `https://example.com/style.css?dpl=v870e20d` + +Cloudflare will attempt to route the request to the specified version. The routing behavior is: + +**If the version is routable** (within TTL, after cutoff, not disabled): +- Request is routed to the specified version + +**If the version is NOT routable** (expired, before cutoff, or manually disabled): +- Request is **automatically rerouted to the active deployment** +- No error is returned to the user +- This ensures graceful degradation when cached version IDs become invalid + +### Enable Skew Protection + +You can enable Skew Protection via API, Dashboard, or Wrangler. + +**Configuration Options:** +- `enabled`: Enable or disable Skew Protection for the Worker +- `version_ttl_hours`: Number of hours versions remain routable after deployment. Default: 48 hours. + +##### Wrangler configuration + + + +```toml +[skew_protection] +enabled = true +version_ttl_hours = 48 +``` + + +##### API Configuration + +Endpoint: PATCH `/accounts/{account_id}/workers/scripts/{script_name}/settings` + +Example request: +``` +curl -X PATCH \ + "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/settings" \ + -H "Authorization: Bearer $API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "skew_protection": { + "enabled": true, + "version_ttl_hours": 48 + } + }' + ``` + + +### List versions with routable status +Endpoint: GET `/accounts/{account_id}/workers/scripts/{script_name}/versions` + +Query parameter: +- `routable` (boolean): Filter to only routable (true) or non-routable (false) versions + +Example: +``` +# Get only routable versions +curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/versions?routable=true" \ + -H "Authorization: Bearer $API_TOKEN" +``` + +### Version cutoff + +Mark a specific version as the cutoff point, making all earlier versions unroutable: + +Endpoint: `POST /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}/cutoff` + +Example request: +``` +curl -X POST \ + "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/versions/va227d9d8/cutoff" \ + -H "Authorization: Bearer $API_TOKEN" \ + -H "Content-Type: application/json" + ``` + +Response: +```json +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "version_id": "va227d9d8", + "cutoff_applied": true, + "cutoff_timestamp": "2025-10-25T12:00:00Z", + "unroutable_versions": [ + { + "version_id": "v3f8c2a1", + "deployed_at": "2025-10-24T12:00:00Z", + "previous_status": "Routable", + "new_status": "Not Routable" + }, + { + "version_id": "v7ee6df6e", + "deployed_at": "2025-10-20T08:00:00Z", + "previous_status": "Routable", + "new_status": "Not Routable" + } + ], + "total_versions_affected": 2 + } +} +``` + +The response includes: +- `cutoff_timestamp`: When the version cutoff was enabled +- `unroutable_versions`: Array of versions that were previously routable but are now unroutable due to the cutoff +- Each affected version shows its `previous_status` and `new_status` + +:::note + +Requests with the `Cloudflare-Workers-Version-Overrides` header or `?dpl= query` parameter targeting versions before the cutoff will be automatically rerouted to the active deployment instead of failing. This ensures that users don't experience errors when their cached version IDs become invalid. + +::: + +### Disable routing for a specific version +Endpoint: `Endpoint: PATCH /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}` + +Example Request: +```bash +curl -X PATCH \ + "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/versions/v8ab1748b" \ + -H "Authorization: Bearer $API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "routable": false + }' + ``` + +### Enable routing for a specific version +Endpoint: `PATCH /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}` +Example request: +```bash +curl -X PATCH \ + "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/versions/v8ab1748b" \ + -H "Authorization: Bearer $API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "routable": true + }' + ``` + +## Framework Maintainers + +If you maintain a Cloudflare adapter for a framework, you can implement Skew Protection by using the version ID that is exposed in the Worker. + +Skew Protection has two parts that work together: +- **Server-side:** Your Worker reads its version ID and sends it to the browser +- **Client-side:** Browser code reads the version ID and includes it in all subsequent requests back to the Worker + +### Server-Side Implementation + +**Step 1: Expose Worker version information** + +Configure the Worker to expose runtime information it needs for skew protection. This will require exposing: +- Version ID: This will be exposed through the version metadata binding and will provide access to the Worker's version ID. +- Worker name: The name of your Worker. This will be used in the header sent on the client side. + +The version metadata binding will expose the Worker's current version ID at runtime through `env.CF_VERSION_METADATA`. Your adapter should add this to the Wrangler config file during the build process. + +For skew protection, you only need the `id` field. + +Additionally, you will need to expose the name of the Worker. You can do this by setting the Worker name as an environment variable. + + + +```toml +# Expose version metadata as CF_VERSION_METADATA binding +[version_metadata] +binding = "CF_VERSION_METADATA" + +# Set the worker name as an environment variable +[vars] +WORKER_NAME = "my-app-worker" +``` + + +**Step 2: Read version ID and Worker name in the Worker** + +In your Worker's `fetch` handler, read the version ID and Worker name from the binding, so you can send it to the client. + +```ts +export default { + async fetch(request, env, ctx) { + // Read version ID + const versionId = env.CF_VERSION_METADATA.id; + + // Pass it to your framework's render function + const html = await framework.render(request, { versionId }); + } +}; +``` +**Step 3: Inject Version ID and Worker name Into HTML** + +Insert the version ID and Worker name into your HTML response so the client can read it. This should happen After your framework renders the HTML, but before returning the response. + +**Example server-side implementation** +```ts +export default { + async fetch(request, env, ctx) { + // Read version ID and worker name + const versionId = env.CF_VERSION_METADATA.id; + const workerName = env.WORKER_NAME; + + // Render your framework's application + let html = await renderFramework(request); + + // Inject version ID and worker name into HTML + html = html.replace('', + `` + ); + + return new Response(html, { + headers: { + 'Content-Type': 'text/html' + } + }); + } +}; +``` + +### Client-side implementation + +The client side is responsible for reading the version information that the server injected into the HTML and including it in all subsequent requests back to the Worker. This ensures that once a user loads a page, all their API calls and asset requests continue to talk to the same Worker version. + +**Understanding what the client needs to do** + +When the page loads, the browser needs to: +1. Read the version ID and worker name that the server injected +2. Intercept all `fetch()` calls to add the version header +3. Modify dynamic asset URLs to add the`?dpl=` query parameter + +#### Understanding the version header format +The `Cloudflare-Workers-Version-Overrides header` uses a Dictionary Structured Header as the format. + +``` +Cloudflare-Workers-Version-Overrides: worker-name="version-id" +Cloudflare-Workers-Version-Overrides: my-app-worker="db7cd8d3-4425-4fe7-8c81-01bf963b6067" +``` + +When Cloudflare receives a request with this header: +1. It parses the worker name and version ID +2. Checks that it can route the request to this version of the Worker +3. If the version specified is "unroutable" then the request will be routed to the current deployment + +*Handling static assets** +The `Cloudflare-Workers-Version-Overrides` header only works for fetch requests. For static assets loaded by HTML tags like `, , and