Deploying to Cloudflare Workers is fundamentally different from deploying to a traditional server. There are no instances to restart, no health checks to wait on, no rolling upgrades across a fleet. Every deployment is atomic and global: your code is either the current version everywhere, or it isn’t deployed yet.

This sounds simple. In practice, it introduces a set of deployment challenges that traditional strategies don’t solve.

How Workers Deployments Work

When you run wrangler deploy, Cloudflare distributes your Worker script to every edge location in their network simultaneously. The switch happens at the request level: requests that were already in-flight to the old version complete against the old code; new requests after the deploy hit the new version.

There is no warm-up period, no cold start on deployment (Workers boot in microseconds), and no intermediate state where some users get v1 and others get v2 based on which server handled their request. The version is determined at deploy time, not at request time.

This is excellent for most things. It becomes interesting when you need gradual rollouts.

Wrangler Versions for Gradual Rollout

Since Wrangler 3.40, Cloudflare has shipped a first-class version management system. Here’s how we use it:

# Upload a new version without routing traffic to it
wrangler versions upload

# This outputs a version ID like:
# Uploaded version with id: "abc123"

# Route 5% of traffic to the new version
wrangler versions deploy --version-percentage abc123=5

# Watch error rates in Cloudflare's analytics
# If clean, ramp up
wrangler versions deploy --version-percentage abc123=25
wrangler versions deploy --version-percentage abc123=100

# Rollback instantly if something goes wrong
wrangler versions deploy --version-percentage abc123=0

The version percentage works at the request level. Roughly 5% (or whatever you set) of incoming requests will be routed to the new version. This is sampled randomly, not by user, so the same user might hit different versions across requests during a gradual rollout. Keep that in mind for stateful operations.

Handling Static Assets

If your Worker serves a frontend (Astro, Next.js static export, etc.), you need to think about asset/code version mismatches during rollouts.

The problem: if you deploy new HTML that references /assets/app.abc123.js but some users still have the old HTML cached, they’ll request the old asset hash which no longer exists.

Content-addressed assets solve this completely. Any build tool worth using (Vite, Astro, Next.js) will output filenames like app.[contenthash].js. Since the filename changes with every build, the old URL still resolves to the old asset. No mismatches possible.

Our wrangler configuration for a static Astro site:

// wrangler.jsonc
{
  "name": "my-worker",
  "compatibility_date": "2024-09-23",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS"
  },
  "main": "src/worker.ts"
}

The ASSETS binding lets your Worker serve static files with automatic content-type detection, range requests, and cache headers, with no custom code needed.

Feature Flags with Workers KV

For feature-level gradual rollout within a single deployed version, Workers KV is the right primitive. Store feature flag state in KV and read it on each request:

interface Env {
  FLAGS: KVNamespace;
}

async function isEnabled(flag: string, userId: string, env: Env): Promise<boolean> {
  const config = await env.FLAGS.get(flag, { type: 'json' }) as {
    enabled: boolean;
    rolloutPercentage?: number;
    allowlist?: string[];
  } | null;

  if (!config || !config.enabled) return false;
  if (config.allowlist?.includes(userId)) return true;
  if (config.rolloutPercentage === undefined) return true;

  // Deterministic per-user rollout using a simple hash
  const hash = [...userId].reduce((acc, char) => acc + char.charCodeAt(0), 0);
  return (hash % 100) < config.rolloutPercentage;
}

The deterministic hash ensures the same user always gets the same flag value during a rollout. They won’t flicker between old and new UI on each page load.

Update flags via the KV API or a simple internal admin endpoint, not a redeployment. This lets you toggle features in seconds without touching your deployment pipeline.

Monitoring During Rollout

Cloudflare’s built-in analytics are not granular enough for deployment monitoring. We use a combination of:

Cloudflare Logpush to stream request logs to a storage bucket in real time. Query these with Cloudflare Workers Analytics Engine or ship them to your observability stack.

Workers Analytics Engine for custom metrics. You can write structured data from inside your Worker:

interface Env {
  ANALYTICS: AnalyticsEngineDataset;
}

function trackRequest(request: Request, response: Response,
                      durationMs: number, env: Env): void {
  env.ANALYTICS.writeDataPoint({
    blobs: [
      new URL(request.url).pathname,
      response.status.toString(),
      request.headers.get('CF-Worker-Version-ID') ?? 'unknown',
    ],
    doubles: [durationMs],
    indexes: [new URL(request.url).hostname],
  });
}

The CF-Worker-Version-ID header (set by Cloudflare automatically when you use version management) lets you segment metrics by version during a gradual rollout, which is exactly what you need to compare v1 vs v2 error rates.

What Zero-Downtime Actually Means

In the Workers model, “zero-downtime” is almost trivially true: there’s no server to restart, so there’s no downtime by definition. The risk is not downtime; it’s correctness. New code that handles requests incorrectly is worse than a brief outage you can roll back from immediately.

These practices (gradual rollout by percentage, feature flags for behavioral changes, and version-segmented metrics) give you the confidence to deploy frequently to production. That confidence is the real goal.