---
title: Development & testing · Cloudflare Workers docs
description: Develop and test your Workers locally.
lastUpdated: 2025-09-19T17:47:58.000Z
chatbotDeprioritize: false
source_url:
  html: https://developers.cloudflare.com/workers/development-testing/
  md: https://developers.cloudflare.com/workers/development-testing/index.md
---

You can build, run, and test your Worker code on your own local machine before deploying it to Cloudflare's network. This is made possible through [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/), a simulator that executes your Worker code using the same runtime used in production, [`workerd`](https://github.com/cloudflare/workerd).

[By default](https://developers.cloudflare.com/workers/development-testing/#defaults), your Worker's bindings [connect to locally simulated resources](https://developers.cloudflare.com/workers/development-testing/#bindings-during-local-development), but can be configured to interact with the real, production resource with [remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings).

## Core concepts

### Worker execution vs Bindings

When developing Workers, it's important to understand two distinct concepts:

* **Worker execution**: Where your Worker code actually runs (on your local machine vs on Cloudflare's infrastructure).

* [**Bindings**](https://developers.cloudflare.com/workers/runtime-apis/bindings/): How your Worker interacts with Cloudflare resources (like [KV namespaces](https://developers.cloudflare.com/kv), [R2 buckets](https://developers.cloudflare.com/r2), [D1 databases](https://developers.cloudflare.com/d1), [Queues](https://developers.cloudflare.com/queues/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), etc). In your Worker code, these are accessed via the `env` object (such as `env.MY_KV`).

## Local development

**You can start a local development server using:**

1. The Cloudflare Workers CLI [**Wrangler**](https://developers.cloudflare.com/workers/wrangler/), using the built-in [`wrangler dev`](https://developers.cloudflare.com/workers/wrangler/commands/#dev) command.

* npm

  ```sh
  npx wrangler dev
  ```

* yarn

  ```sh
  yarn wrangler dev
  ```

* pnpm

  ```sh
  pnpm wrangler dev
  ```

1. [**Vite**](https://vite.dev/), using the [**Cloudflare Vite plugin**](https://developers.cloudflare.com/workers/vite-plugin/).

* npm

  ```sh
  npx vite dev
  ```

* yarn

  ```sh
  yarn vite dev
  ```

* pnpm

  ```sh
  pnpm vite dev
  ```

Both Wrangler and the Cloudflare Vite plugin use [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/) under the hood, and are developed and maintained by the Cloudflare team. For guidance on choosing when to use Wrangler versus Vite, see our guide [Choosing between Wrangler & Vite](https://developers.cloudflare.com/workers/development-testing/wrangler-vs-vite/).

* [Get started with Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
* [Get started with the Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/get-started/)

### Defaults

By default, running `wrangler dev` / `vite dev` (when using the [Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/get-started/)) means that:

* Your Worker code runs on your local machine.
* All resources your Worker is bound to in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) are simulated locally.

### Bindings during local development

[Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) are interfaces that allow your Worker to interact with various Cloudflare resources (like [KV namespaces](https://developers.cloudflare.com/kv), [R2 buckets](https://developers.cloudflare.com/r2), [D1 databases](https://developers.cloudflare.com/d1), [Queues](https://developers.cloudflare.com/queues/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), etc). In your Worker code, these are accessed via the `env` object (such as `env.MY_KV`).

During local development, your Worker code interacts with these bindings using the exact same API calls (such as `env.MY_KV.put()`) as it would in a deployed environment. These local resources are initially empty, but you can populate them with data, as documented in [Adding local data](https://developers.cloudflare.com/workers/development-testing/local-data/).

* By default, bindings connect to **local resource simulations** (except for [AI bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/), as AI models always run remotely).
* You can override this default behavior and **connect to the remote resource** on a per-binding basis with [remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings). This lets you connect to real, production resources while still running your Worker code locally.
* When using `wrangler dev`, you can temporarily disable all [remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings) (and connect only to local resources) by providing the `--local` flag (i.e. `wrangler dev --local`)

## Remote bindings

**Remote bindings** are bindings that are configured to connect to the deployed, remote resource during local development *instead* of the locally simulated resource. Remote bindings are supported by [**Wrangler**](https://developers.cloudflare.com/workers/wrangler/), the [**Cloudflare Vite plugin**](https://developers.cloudflare.com/workers/vite-plugin/), and the `@cloudflare/vitest-pool-workers` package. You can configure remote bindings by setting `remote: true` in the binding definition.

### Example configuration

* wrangler.jsonc

  ```jsonc
  {
    "name": "my-worker",
    "compatibility_date": "2025-11-25",


    "r2_buckets": [
      {
        "bucket_name": "screenshots-bucket",
        "binding": "screenshots_bucket",
        "remote": true,
      },
    ],
  }
  ```

* wrangler.toml

  ```toml
  name = "my-worker"
  compatibility_date = "2025-11-25"


  [[r2_buckets]]
  bucket_name = "screenshots-bucket"
  binding = "screenshots_bucket"
  remote = true
  ```

When remote bindings are configured, your Worker still **executes locally**, only the underlying resources your bindings connect to change. For all bindings marked with `remote: true`, Miniflare will route its operations (such as `env.MY_KV.put()`) to the deployed resource. All other bindings not explicitly configured with `remote: true` continue to use their default local simulations.

### Integration with environments

Remote Bindings work well together with [Workers Environments](https://developers.cloudflare.com/workers/wrangler/environments). To protect production data, you can create a development or staging environment and specify different resources in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) than you would use for production.

**For example:**

* wrangler.jsonc

  ```jsonc
  {
    "name": "my-worker",
    "compatibility_date": "2025-11-25",


    "env": {
      "production": {
        "r2_buckets": [
          {
            "bucket_name": "screenshots-bucket",
            "binding": "screenshots_bucket",
          },
        ],
      },
      "staging": {
        "r2_buckets": [
          {
            "bucket_name": "preview-screenshots-bucket",
            "binding": "screenshots_bucket",
            "remote": true,
          },
        ],
      },
    },
  }
  ```

* wrangler.toml

  ```toml
  name = "my-worker"
  compatibility_date = "2025-11-25"


  [[env.production.r2_buckets]]
  bucket_name = "screenshots-bucket"
  binding = "screenshots_bucket"


  [[env.staging.r2_buckets]]
  bucket_name = "preview-screenshots-bucket"
  binding = "screenshots_bucket"
  remote = true
  ```

Running `wrangler dev -e staging` (or `CLOUDFLARE_ENV=staging vite dev`) with the above configuration means that:

* Your Worker code runs locally
* All calls made to `env.screenshots_bucket` will use the `preview-screenshots-bucket` resource, rather than the production `screenshots-bucket`.

### Recommended remote bindings

We recommend configuring specific bindings to connect to their remote counterparts. These services often rely on Cloudflare's network infrastructure or have complex backends that are not fully simulated locally.

The following bindings are recommended to have `remote: true` in your Wrangler configuration:

#### [Browser Rendering](https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering):

To interact with a real headless browser for rendering. There is no current local simulation for Browser Rendering.

* wrangler.jsonc

  ```jsonc
  {
    "browser": {
      "binding": "MY_BROWSER",
      "remote": true
    },
  }
  ```

* wrangler.toml

  ```toml
  [browser]
  binding = "MY_BROWSER"
  remote = true
  ```

#### [Workers AI](https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai):

To utilize actual AI models deployed on Cloudflare's network for inference. There is no current local simulation for Workers AI.

* wrangler.jsonc

  ```jsonc
  {
    "ai": {
      "binding": "AI",
      "remote": true
    },
  }
  ```

* wrangler.toml

  ```toml
  [ai]
  binding = "AI"
  remote = true
  ```

#### [Vectorize](https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes):

To connect to your production Vectorize indexes for accurate vector search and similarity operations. There is no current local simulation for Vectorize.

* wrangler.jsonc

  ```jsonc
  {
    "vectorize": [
      {
        "binding": "MY_VECTORIZE_INDEX",
        "index_name": "my-prod-index",
        "remote": true
      }
    ],
  }
  ```

* wrangler.toml

  ```toml
  [[vectorize]]
  binding = "MY_VECTORIZE_INDEX"
  index_name = "my-prod-index"
  remote = true
  ```

#### [mTLS](https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates):

To verify that the certificate exchange and validation process work as expected. There is no current local simulation for mTLS bindings.

* wrangler.jsonc

  ```jsonc
  {
    "mtls_certificates": [
      {
        "binding": "MY_CLIENT_CERT_FETCHER",
        "certificate_id": "<YOUR_UPLOADED_CERT_ID>",
        "remote": true
        }
    ]
  }
  ```

* wrangler.toml

  ```toml
  [[mtls_certificates]]
  binding = "MY_CLIENT_CERT_FETCHER"
  certificate_id = "<YOUR_UPLOADED_CERT_ID>"
  remote = true
  ```

#### [Images](https://developers.cloudflare.com/workers/wrangler/configuration/#images):

To connect to a high-fidelity version of the Images API, and verify that all transformations work as expected. Local simulation for Cloudflare Images is [limited with only a subset of features](https://developers.cloudflare.com/images/transform-images/bindings/#interact-with-your-images-binding-locally).

* wrangler.jsonc

  ```jsonc
  {
    "images": {
      "binding": "IMAGES" ,
      "remote": true
    }
  }
  ```

* wrangler.toml

  ```toml
  [images]
  binding = "IMAGES"
  remote = true
  ```

Note

If `remote: true` is not specified for Browser Rendering, Vectorize, mTLS, or Images, Cloudflare **will issue a warning**. This prompts you to consider enabling it for a more production-like testing experience.

If a Workers AI binding has `remote` set to `false`, Cloudflare will **produce an error**. If the property is omitted, Cloudflare will connect to the remote resource and issue a warning to add the property to configuration.

#### [Dispatch Namespaces](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/get-started/developing-with-wrangler/):

Workers for Platforms users can configure `remote: true` in dispatch namespace binding definitions:

* wrangler.jsonc

  ```jsonc
  {
    "dispatch_namespaces": [
      {
        "binding": "DISPATCH_NAMESPACE",
        "namespace": "testing",
        "remote":true
      }
    ]
  }
  ```

* wrangler.toml

  ```toml
  [[dispatch_namespaces]]
  binding = "DISPATCH_NAMESPACE"
  namespace = "testing"
  remote = true
  ```

This allows you to run your [dynamic dispatch Worker](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/#dynamic-dispatch-worker) locally, while connecting it to your remote dispatch namespace binding. This allows you to test changes to your core dispatching logic against real, deployed [user Workers](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/#user-workers).

### Unsupported remote bindings

Certain bindings are not supported for remote connections (i.e. with `remote: true`) during local development. These will always use local simulations or local values.

If `remote: true` is specified in Wrangler configuration for any of the following unsupported binding types, Cloudflare **will issue an error**. See [all supported and unsupported bindings for remote bindings](https://developers.cloudflare.com/workers/development-testing/bindings-per-env/).

* [**Durable Objects**](https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects): Enabling remote connections for Durable Objects may be supported in the future, but currently will always run locally. However, using Durable Objects in combination with remote bindings is possible. Refer to [Using remote resources with Durable Objects and Workflows](#using-remote-resources-with-durable-objects-and-workflows) below.

* [**Workflows**](https://developers.cloudflare.com/workflows/): Enabling remote connections for Workflows may be supported in the future, but currently will only run locally. However, using Workflows in combination with remote bindings is possible. Refer to [Using remote resources with Durable Objects and Workflows](#using-remote-resources-with-durable-objects-and-workflows) below.

* [**Environment Variables (`vars`)**](https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables): Environment variables are intended to be distinct between local development and deployed environments. They are easily configurable locally (such as in a `.dev.vars` file or directly in Wrangler configuration).

* [**Secrets**](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets): Like environment variables, secrets are expected to have different values in local development versus deployed environments for security reasons. Use `.dev.vars` for local secret management.

* [**Static Assets**](https://developers.cloudflare.com/workers/wrangler/configuration/#assets) Static assets are always served from your local disk during development for speed and direct feedback on changes.

* [**Version Metadata**](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/): Since your Worker code is running locally, version metadata (like commit hash, version tags) associated with a specific deployed version is not applicable or accurate.

* [**Analytics Engine**](https://developers.cloudflare.com/analytics/analytics-engine/): Local development sessions typically don't contribute data directly to production Analytics Engine.

* [**Hyperdrive**](https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive): This is being actively worked on, but is currently unsupported.

* [**Rate Limiting**](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/#configuration): Local development sessions typically should not share or affect rate limits of your deployed Workers. Rate limiting logic should be tested against local simulations.

Note

If you have use-cases for connecting to any of the remote resources above, please [open a feature request](https://github.com/cloudflare/workers-sdk/issues) in our [`workers-sdk` repository](https://github.com/cloudflare/workers-sdk).

#### Using remote resources with Durable Objects and Workflows

While Durable Object and Workflow bindings cannot currently be remote, you can still use them during local development and have them interact with remote resources.

There are two recommended patterns for this:

* **Local Durable Objects/Workflows with remote bindings:**

  When you enable remote bindings in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration), your locally running Durable Objects and Workflows can access remote resources. This allows such bindings, although run locally, to interact with remote resources during local development.

* **Accessing remote Durable Objects/Workflows via service bindings:**

  To interact with remote Durable Object or Workflow instances, deploy a Worker that defines those. Then, in your local Worker, configure a remote [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) pointing to the deployed Worker. Your local Worker will be then able to interact with the remote deployed Worker, which in turn can communicate with the remote Durable Objects/Workflows. Using this method, you can create a communication channel via the remote service binding, effectively using the deployed Worker as a proxy interface to the remote bindings during local development.

### Important Considerations

* **Data modification**: Operations (writes, deletes, updates) on bindings connected remotely will affect your actual data in the targeted Cloudflare resource (be it preview or production).

* **Billing**: Interactions with remote Cloudflare services through these connections will incur standard operational costs for those services (such as KV operations, R2 storage/operations, AI requests, D1 usage).

* **Network latency**: Expect network latency for operations on these remotely connected bindings, as they involve communication over the internet.

### API

Wrangler provides programmatic utilities to help tooling authors support remote binding connections when running Workers code with [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/).

**Key APIs include:**

* [`startRemoteProxySession`](#startRemoteProxySession): Starts a proxy session that allows interaction with remote bindings.
* [`unstable_convertConfigBindingsToStartWorkerBindings`](#unstable_convertconfigbindingstostartworkerbindings): Utility for converting binding definitions.
* [`experimental_maybeStartOrUpdateProxySession`](#experimental_maybestartorupdatemixedmodesession): Convenience function to easily start or update a proxy session.

#### `startRemoteProxySession`

This function starts a proxy session for a given set of bindings. It accepts options to control session behavior, including an `auth` option with your Cloudflare account ID and API token for remote binding access.

It returns an object with:

* `ready` Promise\<void>: Resolves when the session is ready.
* `dispose` () => Promise\<void>: Stops the session.
* `updateBindings` (bindings: StartDevWorkerInput\['bindings']) => Promise\<void>: Updates session bindings.
* `remoteProxyConnectionString` remoteProxyConnectionString: String to pass to Miniflare for remote binding access.

#### `unstable_convertConfigBindingsToStartWorkerBindings`

The `unstable_readConfig` utility returns an `Unstable_Config` object which includes the definition of the bindings included in the configuration file. These bindings definitions are however not directly compatible with `startRemoteProxySession`. It can be quite convenient to however read the binding declarations with `unstable_readConfig` and then pass them to `startRemoteProxySession`, so for this wrangler exposes `unstable_convertConfigBindingsToStartWorkerBindings` which is a simple utility to convert the bindings in an `Unstable_Config` object into a structure that can be passed to `startRemoteProxySession`.

Note

This type conversion is temporary. In the future, the types will be unified so you can pass the config object directly to `startRemoteProxySession`.

#### `maybeStartOrUpdateRemoteProxySession`

This wrapper simplifies proxy session management. It takes:

* An object that contains either:

  * the path to a Wrangler configuration and a potential target environment
  * the name of the Worker and the bindings it is using

* The current proxy session details (this parameter can be set to `null` or not being provided if none).

* Potentially the auth data to use for the remote proxy session.

It returns an object with the proxy session details if started or updated, or `null` if no proxy session is needed.

The function:

* Based on the first argument prepares the input arguments for the proxy session.
* If there are no remote bindings to be used (nor a pre-existing proxy session) it returns null, signaling that no proxy session is needed.
* If the details of an existing proxy session have been provided it updates the proxy session accordingly.
* Otherwise if starts a new proxy session.
* Returns the proxy session details (that can later be passed as the second argument to `maybeStartOrUpdateRemoteProxySession`).

#### Example

Here's a basic example of using Miniflare with `maybeStartOrUpdateRemoteProxySession` to provide a local dev session with remote bindings. This example uses a single hardcoded KV binding.

* JavaScript

  ```js
  import { Miniflare, MiniflareOptions } from "miniflare";
  import { maybeStartOrUpdateRemoteProxySession } from "wrangler";


  let mf;


  let remoteProxySessionDetails = null;


  async function startOrUpdateDevSession() {
    remoteProxySessionDetails = await maybeStartOrUpdateRemoteProxySession(
      {
        bindings: {
          MY_KV: {
            type: "kv_namespace",
            id: "kv-id",
            remote: true,
          },
        },
      },
      remoteProxySessionDetails,
    );


    const miniflareOptions = {
      scriptPath: "./worker.js",
      kvNamespaces: {
        MY_KV: {
          id: "kv-id",
          remoteProxyConnectionString:
            remoteProxySessionDetails?.session.remoteProxyConnectionString,
        },
      },
    };


    if (!mf) {
      mf = new Miniflare(miniflareOptions);
    } else {
      mf.setOptions(miniflareOptions);
    }
  }


  // ... tool logic that invokes `startOrUpdateDevSession()` ...


  // ... once the dev session is no longer needed run
  // `remoteProxySessionDetails?.session.dispose()`
  ```

* TypeScript

  ```ts
  import { Miniflare, MiniflareOptions } from "miniflare";
  import { maybeStartOrUpdateRemoteProxySession } from "wrangler";


  let mf: Miniflare | null;


  let remoteProxySessionDetails: Awaited<
    ReturnType<typeof maybeStartOrUpdateRemoteProxySession>
  > | null = null;


  async function startOrUpdateDevSession() {
    remoteProxySessionDetails = await maybeStartOrUpdateRemoteProxySession(
      {
        bindings: {
          MY_KV: {
            type: "kv_namespace",
            id: "kv-id",
            remote: true,
          },
        },
      },
      remoteProxySessionDetails,
    );


    const miniflareOptions: MiniflareOptions = {
      scriptPath: "./worker.js",
      kvNamespaces: {
        MY_KV: {
          id: "kv-id",
          remoteProxyConnectionString:
            remoteProxySessionDetails?.session.remoteProxyConnectionString,
        },
      },
    };


    if (!mf) {
      mf = new Miniflare(miniflareOptions);
    } else {
      mf.setOptions(miniflareOptions);
    }
  }


  // ... tool logic that invokes `startOrUpdateDevSession()` ...


  // ... once the dev session is no longer needed run
  // `remoteProxySessionDetails?.session.dispose()`
  ```

## `wrangler dev --remote` (Legacy)

Separate from Miniflare-powered local development, Wrangler also offers a fully remote development mode via [`wrangler dev --remote`](https://developers.cloudflare.com/workers/wrangler/commands/#dev). Remote development is [**not** supported in the Vite plugin](https://developers.cloudflare.com/workers/development-testing/wrangler-vs-vite/).

* npm

  ```sh
  npx wrangler dev --remote
  ```

* yarn

  ```sh
  yarn wrangler dev --remote
  ```

* pnpm

  ```sh
  pnpm wrangler dev --remote
  ```

During **remote development**, all of your Worker code is uploaded to a temporary preview environment on Cloudflare's infrastructure, and changes to your code are automatically uploaded as you save.

When using remote development, all bindings automatically connect to their remote resources. Unlike local development, you cannot configure bindings to use local simulations - they will always use the deployed resources on Cloudflare's network.

### When to use Remote development

* For most development tasks, the most efficient and productive experience will be local development along with [remote bindings](https://developers.cloudflare.com/workers/development-testing/#remote-bindings) when needed.
* You may want to use `wrangler dev --remote` for testing features or behaviors that are highly specific to Cloudflare's network and cannot be adequately simulated locally or tested via remote bindings.

### Considerations

* Iteration is significantly slower than local development due to the upload/deployment step for each change.

### Limitations

* When you run a remote development session using the `--remote` flag, a limit of 50 [routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) per zone is enforced. Learn more in[ Workers platform limits](https://developers.cloudflare.com/workers/platform/limits/#number-of-routes-per-zone-when-using-wrangler-dev---remote).
