---
title: "Cloudflare Workers Deployment, Done Safely"
description: "A production Cloudflare Workers deployment workflow, from edge-fit assessment through edge canary rollout, Workers observability, and rollback."
canonical_url: https://optimi.com/en/guides/cloudflare-workers-deployment
md_url: https://optimi.com/en/guides/cloudflare-workers-deployment.md
last_updated: 2026-07-15
---

# Cloudflare Workers Deployment, Done Safely

Release a bounded edge handler through isolated environments, a measurable canary, and a known-good version that can be restored without rebuilding under pressure.

A Cloudflare Workers deployment that only has to work once is easy; one that has to work the same way every week, across every edge handler a business depends on, is an operational discipline. That is the same bar a managed edge-orchestration layer holds itself to: staged rollout, measurable canary, and a tested reversal path, applied consistently whether the runtime behind a given route happens to be Cloudflare's or another partner's.

## Outcome and Acme Shop scenario

### Outcome

Acme Shop will deploy a typed, public catalogue Worker first to staging, upload
the production candidate without immediately serving it, then create a small
version split. The release record identifies the stable version, candidate,
stop conditions, and `wrangler rollback` command.

### Running scenario

The Worker serves `GET /edge/catalog` by fetching Acme Shop's public catalogue
API. It does not handle checkout, account, inventory writes, or credentials.
The upstream URL is non-secret configuration; any upstream credential would be
an independently managed Worker secret and is never printed or committed.

**Acme Shop Worker release flow**

1. Stage — Deploy to staging and run synthetic checks.
2. Upload — Upload the production candidate without sending it traffic.
3. Canary — Create a small version split, observe, and promote only when gates pass.
4. Recover — Restore the known-good version when a stop condition is met.

*The old and new version can both serve requests during a gradual deployment, so their contracts must be compatible.*

Cloudflare Workers are a strong fit for request shaping close to users: authentication checks, redirects, cache policy, header normalization, lightweight API composition, and routing between services. They are not a default replacement for every backend. Start by deciding whether the workload is stateless and bounded enough for an edge runtime, then deploy it through explicit environments with a tested reversal path.

This guide is for DevOps and platform engineers operating Workers as production infrastructure. It assumes that the application team owns the handler logic while the platform team owns deployment controls, secrets, traffic exposure, and operational visibility.

## Cloudflare Workers Deployment Prerequisites

Before changing traffic, have the following in place for this Cloudflare Workers deployment:

- A Cloudflare account with permission to create and deploy Workers, manage the target route or custom domain, and read Worker analytics.
- A local installation of Wrangler authenticated to the intended account. Cloudflare documents supported installation and login methods in [Get started with Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/).
- A Git-backed Worker project with a reviewed `wrangler.jsonc`, `wrangler.json`, or `wrangler.toml` configuration.
- Separate non-production and production deployment environments, with distinct secrets and bindings where required.
- A rollback owner, an alert destination, and a known-good version already deployed before enabling a new route or version.
- A decision on Workers observability defaults for each environment: whether Logs are enabled, and at what sampling rate, before the first candidate ships rather than after an incident.

## Stage 1: Confirm That the Workload Fits the Edge

Define the Worker’s contract before choosing bindings or writing a route. A good edge contract has a small, deterministic request path and delegates durable transactions, long-running work, or large data processing to appropriate backend services.

Check the current platform constraints against the proposed behavior:

- Review [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) for CPU time, memory, request, subrequest, and script-size limits. Limits vary by plan and can change, so treat the documentation as the source of truth for the account you will deploy.
- Review [runtime APIs](https://developers.cloudflare.com/workers/runtime-apis/) for compatibility rather than assuming Node.js server APIs are available.
- Ensure any outbound dependency is reachable from the Worker and has explicit timeouts and failure handling in the application code.
- Keep personally identifiable data, credentials, and tenant-specific configuration out of source code and plain-text `vars` when they are secrets.

CPU time and wall-clock time are not the same budget, and conflating them is a common edge-fit mistake. Cloudflare's currently documented Free plan limits CPU time to 10 ms per HTTP request and does not allow that ceiling to be raised; the paid plan defaults to 30 seconds of CPU time per request, configurable up to a 5-minute ceiling. Time spent waiting on `fetch()`, a KV read, a D1 query, or any other network call does not count toward CPU time — only time the CPU spends executing your code does. Acme Shop's `AbortSignal.timeout(1_500)` in the handler below bounds how long the Worker waits on the upstream catalogue API; that is a wall-clock control, not a CPU-time one. A Worker that does heavy synchronous JSON transformation, templating, or cryptography on every response can exhaust its CPU budget before any network timeout fires, so review CPU time against the target plan whenever the handler does non-trivial work on the request or response body, not just when it calls a slow upstream.

### Stateful business logic caveat

Workers can coordinate state through products such as Durable Objects, KV, D1, R2, Queues, and external services, but an individual Worker invocation is not a durable transactional application server. Do not place inventory reservation, payment settlement, account mutation, or other exactly-once business workflows solely in request-local Worker code. Design an authoritative backend, idempotency keys, retry behavior, and a durable audit trail. If using Durable Objects, understand their location and concurrency model from [Durable Objects](https://developers.cloudflare.com/durable-objects/).

## Stage 2: Define Configuration, Secrets, and Bindings

Keep non-secret deployment configuration in the Wrangler file and provision secrets through Wrangler or Cloudflare’s dashboard. Cloudflare explicitly notes that `vars` are not encrypted; use Worker secrets for credentials and tokens. See [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/).

An environment-scoped configuration might look like this:

```toml
name = "edge-gateway"
main = "src/index.ts"
compatibility_date = "2026-07-14"

[env.staging]
name = "edge-gateway-staging"
vars = { API_ORIGIN = "https://api.staging.example.com" }

[[env.staging.kv_namespaces]]
binding = "CONFIG"
id = "staging-namespace-id"

[env.production]
name = "edge-gateway-production"
vars = { API_ORIGIN = "https://api.example.com" }

[[env.production.kv_namespaces]]
binding = "CONFIG"
id = "production-namespace-id"
```

The binding declarations must match the handler’s `env` interface and the target resources must exist in the target account. Use the documentation for the relevant resource type when adding bindings; the [Wrangler configuration reference](https://developers.cloudflare.com/workers/wrangler/configuration/) lists binding configuration and environment inheritance rules.

Set each environment's secret independently. An immediate secret update creates
and deploys a Worker version, so use it only for a non-gradual environment such
as staging:

```bash
wrangler secret put UPSTREAM_TOKEN --env staging
```

For a production gradual deployment, use the version-aware command in the same
reviewed release workflow as the candidate, then deploy that version through
the approved split:

```bash
wrangler versions secret put UPSTREAM_TOKEN --env production
wrangler versions deploy --env production
```

Cloudflare documents that older Wrangler versions may require `--x-versions`.
Confirm the installed version supports `wrangler versions secret put` before the
change window; if it does not, upgrade Wrangler or use Cloudflare's reviewed
version-aware workflow rather than falling back to an immediate production
`wrangler secret put`. Do not put a production secret in a preview, staging, or
repository configuration. Treat secret rotation as an operational change:
deploy code that accepts both old and new credentials if the upstream requires a
staged rotation, then remove the old value after verification.

### Turn on Workers observability before you need it

Workers Logs are not on for a new Worker by default. Add an `observability` block to the Wrangler file and redeploy before the change window, not during an incident:

```toml
[observability]
enabled = true
head_sampling_rate = 1

[env.production.observability]
enabled = true
head_sampling_rate = 0.1
```

`head_sampling_rate` accepts a value from 0 to 1 and defaults to 1 (every invocation logged) when omitted. Acme Shop keeps staging at full sampling, where volume is low and every request matters for the acceptance gate, and lowers production sampling once the catalogue route is stable and high-volume, to control log cost. A lower sampling rate does not have to mean losing visibility into failures: pair head-based sampling with a Tail Worker or a Logpush filter that always forwards non-2xx responses and exceptions regardless of the sampling decision, so a rare error is never sampled away from the dashboard that a canary gate depends on.

### Acme Shop typed handler under review

This handler accepts only the one public read route and does not forward cookies,
authorization headers, or arbitrary client headers to the upstream. It uses a
bounded timeout and returns safe, generic errors. The explicit `no-store` avoids
accidentally creating a shared cache policy in the Worker; cache policy can be
reviewed separately.

```ts
export interface Env {
  API_ORIGIN: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)

    if (url.pathname !== "/edge/catalog") return new Response("Not found", { status: 404 })
    if (request.method !== "GET") return new Response("Method not allowed", { status: 405 })

    const upstream = new URL("/catalog", env.API_ORIGIN)
    upstream.search = url.search

    try {
      const response = await fetch(upstream, {
        headers: { Accept: "application/json" },
        signal: AbortSignal.timeout(1_500),
      })

      if (!response.ok) return new Response("Catalogue temporarily unavailable", { status: 502 })

      return new Response(response.body, {
        headers: {
          "cache-control": "no-store",
          "content-type": response.headers.get("content-type") ?? "application/json",
        },
      })
    } catch {
      return new Response("Catalogue temporarily unavailable", { status: 503 })
    }
  },
} satisfies ExportedHandler<Env>
```

Use an `Env` interface that matches every declared binding. Keep the staging
origin distinct from production and have TypeScript validate the handler before
deploying. If a future route needs an authenticated upstream, use a Worker
secret and a narrowly scoped server-to-server contract; do not pass client
credentials through the Worker.

## Stage 3: Deploy to an Isolated Environment

Run static checks and application tests first. Then validate the resolved Wrangler configuration and deploy the staging environment:

```bash
wrangler deploy --env staging
```

Wrangler environments let one project declare separate deployment targets. Confirm the deployed worker name, account, routes, bindings, compatibility date, and any environment-specific settings in the command output and Cloudflare dashboard. The [environments guide](https://developers.cloudflare.com/workers/wrangler/environments/) explains what is and is not inherited by named environments; do not assume a production binding or variable is automatically present in staging.

If the Worker is attached to a route, verify route precedence and that the route pattern is intentionally narrow. A broad pattern can intercept traffic that was previously served by another Worker or by the origin. For custom domains and routes, use the documented [routing configuration](https://developers.cloudflare.com/workers/configuration/routing/).

## Stage 4: Test Safely Before Production

Use a non-production hostname or a deliberately narrow staging route. Exercise the request path end-to-end, not only a local handler test.

Test at least the following:

- Expected success responses, methods, headers, cache behavior, and CORS behavior.
- Missing or invalid credentials, malformed inputs, and authorization boundaries.
- Upstream timeouts, DNS failures, `5xx` responses, and rate-limit responses.
- Every binding path, including behavior when optional configuration is absent.
- Route matching with representative production-like URLs, including paths that must bypass the Worker.
- The response and logging behavior when a secret or required binding is deliberately unavailable in staging.

Use the following command for fast local feedback:

```bash
wrangler dev
```

Do not treat local development as equivalent to production. Cloudflare distinguishes local and remote development modes in [Wrangler development](https://developers.cloudflare.com/workers/wrangler/commands/#dev); remote dependencies, routes, bindings, and account configuration still require an environment deployment test.

## Stage 5: Workers Observability in Staging

Instrument the Worker with structured, non-sensitive events that identify the route, outcome class, upstream dependency, and deployment version. Avoid logging authorization headers, cookies, request bodies, access tokens, or customer identifiers.

During the test window, inspect:

- Worker error rate and exception samples in the dashboard.
- Request volume and response status distribution compared with the known-good path.
- CPU time and other resource signals that may approach documented limits.
- Upstream latency and error metrics from the authoritative service, since a successful Worker response can still conceal degraded origin behavior.
- Inspect logs when live request-level diagnosis is needed:

  ```bash
  wrangler tail --env staging
  ```

Cloudflare documents available telemetry and tailing workflows in [Observability](https://developers.cloudflare.com/workers/observability/). Retain metrics and logs in your central observability platform if the incident process requires longer retention or correlation with origin signals.

### Choosing where the telemetry lives

Workers observability spans four distinct mechanisms, and picking the wrong one for the question you are asking wastes the observation window:

- **Workers Logs** in the Cloudflare dashboard, once enabled per the previous stage, is the fastest path to a filtered, queryable view of a single Worker's recent invocations without any external system.
- **`wrangler tail`** streams events live during a deploy or an incident; it is a diagnostic session, not a retention mechanism, and stops the moment the terminal closes.
- **Logpush** exports Workers Trace Event Logs continuously to R2, S3, or a third-party log destination, which is the mechanism to reach for when the acceptance criteria require retention beyond the dashboard's window or joins against origin or WAF logs stored elsewhere.
- **OpenTelemetry export** sends Workers trace data over OTLP to an external stack such as Grafana, Honeycomb, or Axiom, which matters once the catalogue Worker is one hop in a request that also touches other services and the useful unit of analysis is the whole trace, not one Worker's logs in isolation.

Pick the mechanism that matches the question in front of you: a live tail answers "what is happening right now," Workers Logs answers "what happened to this Worker recently," and Logpush or OTel export answer "what happened across this system over the retention period the incident process requires."

## Stage 6: Run an Edge Canary Rollout

Deploy the production environment only after staging has met its acceptance criteria:

```bash
wrangler deploy --env production
```

For a versioned canary, do not use the immediate `wrangler deploy` path above
for the candidate. Cloudflare's current gradual-deployment workflow uploads a
version without deploying it, then creates a split deployment interactively.
Confirm the installed Wrangler version supports this workflow before the change
window.

```sh
wrangler versions upload --env production
wrangler versions deploy --env production
```

Choose the known-good and candidate version IDs in the interactive deployment,
start with the smallest exposure justified by traffic volume, and record the
chosen percentages. A split is per request by default, so consecutive requests
can reach different versions. Keep API and Durable Object changes backward and
forward compatible; a Durable Object migration cannot be safely handled as an
ordinary gradual deployment rollback.

**Representative Wrangler output; version IDs shortened**

```
Uploaded edge-gateway-production version: 3a0f...c91e
No deployment changed.

Deployment created
  95%  version 81d2...0a44  (known good)
   5%  version 3a0f...c91e  (candidate)
```

Prefer a limited exposure mechanism before a full route cutover. Depending on the architecture, that can be a dedicated canary hostname, a narrowly scoped route, a controlled cohort chosen by the application, or Cloudflare [Worker versions and deployments](https://developers.cloudflare.com/workers/versions-and-deployments/) when available for the account and deployment model. Do not claim a fixed latency improvement or assume traffic splitting removes the need for functional monitoring; cache location, origin behavior, code paths, and client geography determine observed results.

> **A canary is only as trustworthy as the telemetry watching it**
>
> Attribute every metric, log line, and alert to a specific Worker version before trusting a percentage split — a dashboard that blends versions together will hide a regression inside an average. This is the same discipline a managed observability layer such as MYO applies when an edge canary rollout runs behind more than one provider: normalize version and deployment identifiers into one view so a 5% candidate on Cloudflare is held to the same gates as a 5% candidate anywhere else in the estate, instead of being judged from a provider-specific dashboard that only shows part of the picture.

### Automate the split for a repeatable canary

An interactive prompt is fine for a one-off release, but a repeatable process should script the split so the same version IDs and percentages are reviewed in the change record rather than chosen ad hoc at a terminal. Once both version IDs are known, Wrangler accepts a non-interactive form:

```bash
wrangler versions deploy \
  81d2f0a1-3b7c-4e21-9a5d-000000000a44@95% \
  3a0fbb21-7c4d-4a6e-8b12-0000000c91e0@5% \
  --message "Acme Shop catalogue canary, step 1" \
  -y
```

Keep the `--message` field meaningful; it becomes part of the deployment history the dashboard and API expose, and it is the fastest way for an on-call responder to see what changed without re-deriving it from version IDs. Template this command from the release record and drive it from whatever change-management step already gates a production Cloudflare Workers deployment, rather than leaving the exposure percentage for someone to type correctly by hand.

### Version skew, affinity, and stateful edge cases

A percentage split is evaluated per request by default, so the same browser tab can hit the known-good version on one request and the candidate on the next. For Acme Shop's stateless catalogue read this is harmless; it becomes a real defect class once a route touches sessions, multi-step forms, or content-hashed static assets that must match the HTML page that referenced them. Cloudflare's version affinity mechanism pins a caller to one version by hashing a value carried in the `Cloudflare-Workers-Version-Key` header — typically set from a session cookie, an authenticated user identifier, or the client IP for anonymous traffic — so consecutive requests from the same caller stay on one version until they cross over as the candidate's percentage grows, and callers already on the newer version do not flip back without an explicit rollback. Affinity is normally set through a zone Transform Rule for traffic on a controlled route; it is not available on a bare `*.workers.dev` hostname unless the header is set by the client or an upstream Worker.

**Version affinity keeps a caller on one Worker version**

![A sequence diagram first shows two requests from the same browser reaching different stable and candidate Worker versions under a per-request percentage split. It then shows a version key being hashed at the edge so consecutive requests from that browser reach the same candidate version until an explicit rollback or a later deployment change.](/diagrams/cloudflare-workers-deployment/version-affinity.svg)

*Per-request splits suit stateless routes. Use version affinity when a caller must not alternate between compatible but different Worker versions.*

Durable Objects introduce a second, unrelated form of skew. Because each Durable Object instance is a single globally unique actor, code changes roll out in an eventually consistent way: a request can reach the new Worker version while the Durable Object it calls is still running the previous version for a short window, typically seconds to minutes. A new version deploy also restarts every Durable Object it touches, which disconnects any WebSocket currently open on it. Treat a canary on a Durable-Object-backed Worker the same way as a Durable Object migration: confirm the RPC or storage contract is compatible in both directions before exposure, and expect connected clients to reconnect during the release rather than assuming a canary is transparent to long-lived connections.

Define explicit canary gates before exposure:

- No material increase in Worker exceptions or `5xx` responses compared with the baseline.
- No breach of upstream service error or saturation thresholds.
- No regression in critical synthetic checks or authenticated user journeys.
- Resource use remains below the operational safety margin chosen from Cloudflare’s current limits.

Increase exposure only after a sustained observation period appropriate to request volume. Low-volume services need longer observation or synthetic traffic to generate meaningful evidence.

### Validation matrix

| Check | Expected result | Stop and investigate when |
| --- | --- | --- |
| Positive: public catalogue `GET` | `200` JSON response, expected schema, `Cache-Control: no-store` | Content type, schema, or latency budget regresses |
| Negative: `POST /edge/catalog` | `405`; upstream is not called | The Worker forwards or mutates an unsupported request |
| Negative: `/checkout` | `404` from this Worker route; checkout remains on its intended path | A broad route intercepts checkout or account traffic |
| Failure: staging catalogue API timeout | `503` with no credentials or upstream details exposed | Unbounded wait, exception spike, or sensitive diagnostic response |
| Canary: version split | Candidate metrics stay within agreed error and latency gates | Candidate errors, origin saturation, or synthetic failures rise |

## Rollback

Prepare the reversal command and decision threshold before the production deploy. For a route-based release, remove or narrow the new route only if doing so restores the prior known-good handler or origin path. For a versioned Worker deployment, roll traffic back to the known-good version using the Cloudflare-supported deployment workflow. Refer to [rollbacks for Worker versions](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) for the current command and dashboard behavior.

`wrangler rollback [<version-id>]` is the versioned path: it immediately creates a new deployment that routes 100% of traffic to the version you name, or to the previous version if omitted, replacing whatever split was active rather than adjusting it. Rollback does not touch bound resources — KV namespaces, R2 buckets, D1 databases, queues, and secrets stay exactly as configured — so it is only safe against data drift if the shape the older code expects has not changed underneath it. Cloudflare blocks the rollback outright if a Durable Object migration happened between the active deployment and the target version, or if the target binds to a KV namespace, R2 bucket, or queue that no longer exists; treat either case as a forward fix, not a rollback. Only the 100 most recently published versions remain eligible targets, so periodically confirm a relied-upon fallback version has not aged out of that window on a Worker that deploys frequently. For a scripted runbook, pass an explicit message to skip the interactive confirmation:

```bash
wrangler rollback 81d2f0a1-3b7c-4e21-9a5d-000000000a44 \
  --message "Acme Shop catalogue rollback: canary error-rate gate breached"
```

After rollback:

1. Confirm the known-good path is receiving requests.
2. Re-run synthetic and representative endpoint checks.
3. Keep observing both Worker and origin errors until they return to baseline.
4. Preserve the failed deployment identifier, configuration diff, logs, and timeline for incident review.

Do not use a rollback as a substitute for data repair. If the release performed stateful effects, reconcile those effects in the system of record using the business workflow’s documented recovery procedure.

## Troubleshooting

| Symptom | Likely cause | Safe diagnostic | Recovery |
| --- | --- | --- | --- |
| Candidate serves `404` unexpectedly | Route pattern or environment route was not deployed | Inspect the resolved production route and test the exact canary URL | Correct the narrow route, validate in staging, then upload a new candidate |
| Valid request becomes `502` | Upstream returned a non-success response | Compare non-sensitive Worker status metrics with upstream status metrics | Restore the stable deployment; fix the upstream contract before retrying |
| Requests time out at the edge | Upstream is slow or timeout budget is too small | Review Worker and origin latency without logging request bodies | Roll back the candidate or adjust the reviewed bounded timeout after staging tests |
| Canary metrics mix versions | No version attribution or per-request version skew | Use deployment IDs and version-aware telemetry | Add safe version metadata to observability and keep versions contract-compatible |
| Rollback fails or is unsafe | Binding/resource changed, or Durable Object migration occurred | Compare candidate and stable bindings/migrations before action | Use the documented forward fix or resource recovery; do not force an incompatible code rollback |
| WebSocket clients drop mid-rollout | A version deploy restarted the Durable Object backing the connection | Confirm the timing against the deployment log, not client-side retry noise | Design for reconnect on any Durable-Object-backed release; do not treat it as a Worker defect |
| Canary looks clean but incidents still surface | Log sampling is hiding the failures the dashboard would otherwise show | Check the effective `head_sampling_rate` for the environment and any Tail Worker error-forwarding filter | Raise sampling for the observation window or add a filter that always forwards non-2xx responses and exceptions |

## Related guides

- [Cloudflare Cache Rules](/en/guides/cloudflare-cache-rules)
- [Cloudflare WAF Configuration](/en/guides/cloudflare-waf-configuration)
- [Cloudflare Load Balancing](/en/guides/cloudflare-load-balancing)

## Technical Caveats

Worker behavior depends on the Cloudflare plan, compatibility date, enabled products, account configuration, and the specific region and dependency path serving a request. Review Cloudflare’s current limits and product documentation at each material design or platform upgrade. Edge execution can reduce work at the origin for suitable requests, but it does not guarantee lower end-to-end latency, availability, or consistency for every request.

## Primary Sources

- [Cloudflare Workers limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Cloudflare Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
- [Cloudflare Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/)
- [Cloudflare Workers secrets](https://developers.cloudflare.com/workers/configuration/secrets/)
- [Cloudflare Workers observability](https://developers.cloudflare.com/workers/observability/)
- [Cloudflare Worker versions, deployments, and rollbacks](https://developers.cloudflare.com/workers/versions-and-deployments/)
- [Cloudflare Workers gradual deployments and version affinity](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/)
- [Cloudflare Workers Logs configuration](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)

[Discuss edge delivery](/en/contact): Make every Cloudflare Workers deployment safer to operate — Talk to Optimi about edge canary rollout gates, Workers observability, and delivery visibility across Performance, Security, and Visibility for critical application paths.
