How to use Cloudflare Worker Previews

By

Cloudflare Worker Previews give every Git branch its own URL, settings and Durable Object storage. How to create one with wrangler preview and what to watch.

~~~

Worker Previews give every Git branch its own running copy of your Cloudflare Worker, with its own URL, its own settings, and its own Durable Object storage. It lives under the same Worker, next to production. You create one by running npx wrangler preview on a branch.

Cloudflare launched them on September 22, 2026, as an open beta. Cloudflare Pages has had branch previews for years, and now Workers get the same thing.

Let’s build a small Worker, deploy it, and test a change on a branch without touching production.

Why Previews?

Before Previews, you had two options, and neither was great.

Version URLs (they used to be called preview URLs) run an uploaded version of your code against the production resources. Test a signup form there and the test user lands in your production database. And if your Worker has a Durable Object, you don’t get a Version URL at all.

Wrangler environments create a separate Worker, like my-worker-staging. You end up with one staging Worker that everyone shares.

With Previews, each branch gets its own environment. If you’re new to Workers, start with my Cloudflare Workers tutorial and the Wrangler guide.

Build a visit counter

Our example counts visits and stores the count in a Durable Object. That way we can see that a preview doesn’t share state with production. My Durable Objects tutorial explains them from scratch.

Previews need Wrangler 4.135 or later, installed in the project. npx wrangler doesn’t use your global install, so an older version pinned in package.json is why wrangler preview won’t work:

mkdir visit-counter
cd visit-counter
git init -b main
npm init -y
npm i -D wrangler@latest

Save this as src/index.js:

import { DurableObject } from 'cloudflare:workers'

export class Counter extends DurableObject {
  async increment() {
    const visits = ((await this.ctx.storage.get('visits')) ?? 0) + 1
    await this.ctx.storage.put('visits', visits)
    return visits
  }
}

export default {
  async fetch(request, env, ctx) {
    const counter = ctx.exports.Counter.getByName('homepage')
    const visits = await counter.increment()

    return Response.json({
      environment: env.ENVIRONMENT,
      visits,
    })
  },
}

And this as wrangler.jsonc:

{
  "name": "visit-counter",
  "main": "src/index.js",
  "compatibility_date": "2026-09-22",
  "vars": {
    "ENVIRONMENT": "production"
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] }
  ],
  "previews": {
    "vars": {
      "ENVIRONMENT": "preview"
    }
  }
}

The top-level settings are for production. The previews block is for previews, and here it only changes ENVIRONMENT, so the response tells us where we are.

Commit and deploy to production:

git add -A
git commit -m "visit counter"
npx wrangler deploy

I called it three times:

curl https://visit-counter.flaviocopes.workers.dev
{"environment":"production","visits":1}
{"environment":"production","visits":2}
{"environment":"production","visits":3}

Create a preview

Now let’s say we want to count visits per page. We create a branch:

git checkout -b count-by-page

and use the path as the object name, so each page gets its own counter:

export default {
  async fetch(request, env, ctx) {
    const { pathname } = new URL(request.url)
    const counter = ctx.exports.Counter.getByName(pathname)
    const visits = await counter.increment()

    return Response.json({
      environment: env.ENVIRONMENT,
      page: pathname,
      visits,
    })
  },
}

Commit, and run preview instead of deploy:

git commit -am "count visits per page"
npx wrangler preview
Preview: count-by-page (new)
Preview URL: https://count-by-page-visit-counter.flaviocopes.workers.dev
Unique Deployment URL: https://ae944cdf-visit-counter.flaviocopes.workers.dev

The preview is named after the branch. The Preview URL always serves the latest push to the branch, while each deployment URL points to one specific deploy.

Let’s call the preview, then production:

{"environment":"preview","page":"/","visits":1}
{"environment":"preview","page":"/","visits":2}
{"environment":"preview","page":"/blog","visits":1}
{"environment":"production","visits":4}

The preview runs the new code with its own settings, and its counter started from 1. Production didn’t notice anything.

That’s because Cloudflare creates a new Durable Object namespace for every preview. We didn’t configure anything for it: with ctx.exports, the class and its migration are enough. If a preview shared production’s namespace, a bad change on a branch could corrupt live data.

Push again to the same branch, run npx wrangler preview again, and the same preview gets updated. Its state stays until you delete it:

npx wrangler preview delete --name count-by-page

Previews don’t inherit production settings

This is the one thing to remember. A preview only gets what’s in the previews block. Durable Objects are the exception, everything else you have to add.

Let’s see what happens when we forget. We add a KV namespace for a banner message, with a binding at the top level only:

"kv_namespaces": [
  { "binding": "SETTINGS", "id": "a6b3af381ab849d6948fe7e664835778" }
],

and read it in the Worker with await env.SETTINGS.get('banner'). When we run the preview, Wrangler warns us:

▲ [WARNING] These bindings are configured for your production Worker but not for Previews:

    SETTINGS  KV Namespace

It deploys anyway, and the preview answers with:

error code: 1101

env.SETTINGS doesn’t exist in the preview, so the Worker throws. The fix is to add the same binding inside previews, pointed at a separate namespace for testing (my KV guide shows how to create one):

"previews": {
  "vars": {
    "ENVIRONMENT": "preview"
  },
  "kv_namespaces": [
    { "binding": "SETTINGS", "id": "96cefe7e8de84b76a3e93c0e27eca3f6" }
  ]
}

Now the preview reads its own test banner, and production keeps reading the real one (the counter kept going across the deploys in between):

{"environment":"preview","page":"/","banner":"Testing the new banner","visits":9}

The same goes for D1, R2, Queues and the other bindings. Two previews bound to the same database share its rows, so point them at a staging database, not the production one.

Secrets can’t go in the config file, so they have their own commands. npx wrangler preview base-config secret put RESEND_API_KEY sets a secret for every new preview, and npx wrangler preview secret put RESEND_API_KEY sets it for the current branch only. Production is never touched.

Previews on every push

You don’t have to run the command by hand. If your Worker is connected to Git through Workers Builds, enable Preview Builds in the Worker’s build settings. Every push to a branch creates or updates its preview, and Cloudflare comments the URL on the pull request.

In any other CI, run the same command with a name and --json, and read the URL from the output:

npx wrangler preview --name pr-42 --json | jq -r '.preview.urls[0]'

The Previews examples page has a complete GitHub Actions workflow that comments the URL and deletes the preview when the pull request closes.

Previews are public by default. You can serve them on your own domain, which helps with cookies and OAuth redirects, and put Cloudflare Access in front of them to require a login. Each preview also has its own logs and traces in the dashboard, separate from production. My Workers observability post covers how those work.

Keep previews out of Google

A preview is a full copy of your site on another URL. If Google finds one, through a link in a public pull request, an issue or a chat, it can index it.

That causes a few problems. The preview competes with your real pages as duplicate content. Unreleased features and test data show up in search results. And the result keeps pointing to a URL that returns a 404 once you delete the preview.

On workers.dev Cloudflare already handles this. Both the Preview URL and the deployment URLs send this header, while production doesn’t:

x-robots-tag: noindex

It tells search engines not to index the page. I haven’t seen this documented, so don’t count on it for previews served on your own domain. There you can add the header yourself, only in previews:

const response = Response.json({
  environment: env.ENVIRONMENT,
  page: pathname,
  visits,
})

if (env.ENVIRONMENT === 'preview') {
  response.headers.set('X-Robots-Tag', 'noindex')
}

return response

Don’t use a robots.txt that blocks everything for this. A blocked crawler never sees the noindex header, and Google can still list a blocked URL it found through a link.

The most reliable option is Cloudflare Access. A preview behind a login can’t be crawled at all, and it keeps your unreleased work private too. My robots.txt guide covers how robots.txt rules work.

Previews and coding agents

Cloudflare pitched this launch at coding agents, and it fits. An agent working on a branch can deploy a real copy of the app, test it with curl or a browser, read its errors, fix them, and deploy again, with no way to break production.

A few lines in your AGENTS.md are enough:

## Testing changes

- Never run `npx wrangler deploy`. Production deploys happen on merge.
- To test a change, run `npx wrangler preview --json` and open `.preview.urls[0]`.
- If Wrangler warns that bindings are missing for Previews, add them to the `previews` block in `wrangler.jsonc`.

What doesn’t work yet

Some parts of a Worker still point at production:

Cloudflare says multi-Worker previews and Queues and Workflows inside previews are coming. The resources page keeps the current list.

Which one should you use?

WorkflowCommandUse it for
Previewsnpx wrangler previewBranches and pull requests
Version URLsnpx wrangler versions uploadChecking one exact version, with production data
Wrangler environmentsnpx wrangler deploy --env stagingA long-lived staging Worker

For branches, use Previews. Keep environments for a staging site that lives on its own. My post on Workers secrets and environments explains how they work.

How I would use it

This site runs on Cloudflare Pages, which has its own preview deployments, so nothing changes here.

Where I’d use it is a project like Waiting Lists, which runs on Workers with D1. I’d point every preview at one staging database and test changes to the signup flow on the Preview URL before merging. Its email delivery events go through a Queue consumer and its cleanup runs on a Cron Trigger, though, and neither runs in a preview, so I’d still test those locally with wrangler dev.

If you want to go deeper, the free Cloudflare Workers course builds a full application on Workers, step by step.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cloudflare: