Skew Protection
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.
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-Overridesheader 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
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.
Cloudflare identifies which version to route requests to using the following mechanisms, in priority order:
-
Query parameter:
?dpl=<version-id>(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:
<img src="/logo.png?dpl=v870e20d"> - Example:
<script src="/_next/static/chunks/main.js?dpl=v870e20d"></script>
-
HTTP header:
Cloudflare-Workers-Version-Overrides: <worker-name>="<version-id>"(fallback)- Best for API calls, fetch requests, or any request where you can control the HTTP headers
- Uses Dictionary Structured Header ↗ 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.
When a request includes version routing information:
- Header-based routing:
Cloudflare-Workers-Version-Overrides: my-worker="v870e20d" - 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
You can enable Skew Protection via API, Dashboard, or Wrangler.
Configuration Options:
enabled: Enable or disable Skew Protection for the Workerversion_ttl_hours: Number of hours versions remain routable after deployment. Default: 48 hours.
{ "skew_protection": { "enabled": true, "version_ttl_hours": 48 }}[skew_protection]enabled = trueversion_ttl_hours = 48Endpoint: 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 } }'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 versionscurl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/versions?routable=true" \ -H "Authorization: Bearer $API_TOKEN"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:
{ "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 enabledunroutable_versions: Array of versions that were previously routable but are now unroutable due to the cutoff- Each affected version shows its
previous_statusandnew_status
Endpoint: Endpoint: PATCH /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}
Example Request:
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 }'Endpoint: PATCH /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}
Example request:
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 }'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
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.
{ "version_metadata": { "binding": "CF_VERSION_METADATA" }, "vars": { "WORKER_NAME": "my-app-worker" }}# 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.
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
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('</head>', `<script> window.__CF_WORKER_VERSION__="${versionId}"; window.__CF_WORKER_NAME__="${workerName}"; </script></head>` );
return new Response(html, { headers: { 'Content-Type': 'text/html' } }); }};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:
- Read the version ID and worker name that the server injected
- Intercept all
fetch()calls to add the version header - Modify dynamic asset URLs to add the
?dpl=query parameter
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:
- It parses the worker name and version ID
- Checks that it can route the request to this version of the Worker
- 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 <img>, <link>, and <script>, browsers don't allow you to add custom headers. Instead, you must add the version as a query parameter: ?dpl=<version-id>
Example server-side implementation
const versionId = window.__CF_WORKER_VERSION__;const workerName = window.__CF_WORKER_NAME__;
if (versionId && workerName) { // Build the version header const versionHeader = `${workerName}="${versionId}"`;
// Intercept fetch to add version header const originalFetch = window.fetch; window.fetch = function(url, options = {}) { options.headers = { ...options.headers, 'Cloudflare-Workers-Version-Overrides': versionHeader }; return originalFetch(url, options); };
// Handle static assets // Framework-specific: modify asset URLs to include ?dpl= parameter // Example: /image.png becomes /image.png?dpl=abc123}Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark