# How to use Cloudflare Worker Previews

> 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.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-09-24 | Updated: 2026-09-24 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/cloudflare-worker-previews/

**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](https://flaviocopes.com/cloudflare-workers/) and the [Wrangler guide](https://flaviocopes.com/cloudflare-wrangler/).

## 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](https://flaviocopes.com/cloudflare-durable-objects/) 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:

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

Save this as `src/index.js`:

```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`:

```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:

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

I called it three times:

```bash
curl https://visit-counter.flaviocopes.workers.dev
```

```text
{"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:

```bash
git checkout -b count-by-page
```

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

```js
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`:

```bash
git commit -am "count visits per page"
npx wrangler preview
```

```text
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:

```text
{"environment":"preview","page":"/","visits":1}
{"environment":"preview","page":"/","visits":2}
{"environment":"preview","page":"/blog","visits":1}
```

```text
{"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:

```bash
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:

```jsonc
"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:

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

    SETTINGS  KV Namespace
```

It deploys anyway, and the preview answers with:

```text
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](https://flaviocopes.com/cloudflare-kv/) shows how to create one):

```jsonc
"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):

```text
{"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:

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

The [Previews examples page](https://developers.cloudflare.com/workers/previews/examples/) 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](https://flaviocopes.com/cloudflare-workers-observability/) 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:

```text
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:

```js
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](https://flaviocopes.com/robots-txt-ai-crawlers/) 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:

```markdown
## 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:

- Service bindings from a preview call the other Worker's production deployment.
- Queue consumers, Cron Triggers and routes only run in production.
- Workflows aren't created per preview.

Cloudflare says multi-Worker previews and Queues and Workflows inside previews are coming. The [resources page](https://developers.cloudflare.com/workers/previews/resources/) keeps the current list.

## Which one should you use?

| Workflow | Command | Use it for |
| --- | --- | --- |
| Previews | `npx wrangler preview` | Branches and pull requests |
| Version URLs | `npx wrangler versions upload` | Checking one exact version, with production data |
| Wrangler environments | `npx wrangler deploy --env staging` | A 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](https://flaviocopes.com/cloudflare-workers-secrets-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](https://flaviocopes.com/software/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](https://flaviocopes.com/courses/cloudflare-workers/) builds a full application on Workers, step by step.
