Development & testing
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, a simulator that executes your Worker code using the same runtime used in production, workerd
↗.
By default, your Worker's bindings connect to locally simulated resources, but can be configured to interact with the real, production resource with remote 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: How your Worker interacts with Cloudflare resources (like KV namespaces, R2 buckets, D1 databases, Queues, Durable Objects, etc). In your Worker code, these are accessed via the
env
object (such asenv.MY_KV
).
You can start a local development server using:
- The Cloudflare Workers CLI Wrangler, using the built-in
wrangler dev
command.
npx wrangler dev
yarn wrangler dev
pnpm wrangler dev
- Vite ↗, using the Cloudflare Vite plugin.
npx vite dev
yarn vite dev
pnpm vite dev
Both Wrangler and the Cloudflare Vite plugin use 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.
By default, running wrangler dev
/ vite dev
(when using the Vite plugin) means that:
- Your Worker code runs on your local machine.
- All resources your Worker is bound to in your Wrangler configuration are simulated locally.
Bindings are interfaces that allow your Worker to interact with various Cloudflare resources (like KV namespaces, R2 buckets, D1 databases, Queues, 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.
- By default, bindings connect to local resource simulations (except for AI bindings, as AI models always run remotely).
- You can override this default behavior and connect to the remote resource, on a per-binding basis. This lets you connect to real, production resources while still running your Worker code locally.
Remote bindings are bindings that are configured to connect to the deployed, remote resource during local development instead of the locally simulated resource. You can configure remote bindings by setting experimental_remote: true
in the binding definition.
{ "name": "my-worker", "compatibility_date": "2025-06-18",
"r2_buckets": [ { "bucket_name": "screenshots-bucket", "binding": "screenshots_bucket", "experimental_remote": true, }, ],}
name = "my-worker"compatibility_date = "2025-06-18"
[[r2_buckets]]bucket_name = "screenshots-bucket"binding = "screenshots_bucket"experimental_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 experimental_remote: true
, Miniflare will route its operations (such as env.MY_KV.put()
) to the deployed resource. All other bindings not explicitly configured with experimental_remote: true
continue to use their default local simulations.
If you're using Wrangler for local development and have remote bindings configured, you'll need to use the following experimental command:
npx wrangler dev --x-remote-bindings
yarn wrangler dev --x-remote-bindings
pnpm wrangler dev --x-remote-bindings
If you're using Vite via the Cloudflare Vite plugin, you'll need to add support for remote bindings in your Vite configuration (vite.config.ts
):
import { cloudflare } from "@cloudflare/vite-plugin";import { defineConfig } from "vite";
export default defineConfig({ plugins: [ cloudflare({ configPath: "./entry-worker/wrangler.jsonc", experimental: { remoteBindings: true }, }), ],});
You can also use Vitest with configured remote bindings by enabling support in your Vitest configuration file (vitest.config.ts
):
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ test: { poolOptions: { workers: { experimental_remoteBindings: true, wrangler: { configPath: "./wrangler.jsonc" }, }, }, },});
To protect production data, you can create and specify preview resources in your Wrangler configuration, such as:
- Preview namespaces for KV stores:
preview_id
. - Preview buckets for R2 storage:
preview_bucket_name
. - Preview database IDs for D1:
preview_database_id
If preview configuration is present for a binding, setting experimental_remote: true
will ensure that remote bindings connect to that designated remote preview resource.
For example:
{ "name": "my-worker", "compatibility_date": "2025-06-18",
"r2_buckets": [ { "bucket_name": "screenshots-bucket", "binding": "screenshots_bucket", "preview_bucket_name": "preview-screenshots-bucket", "experimental_remote": true, }, ],}
name = "my-worker"compatibility_date = "2025-06-18"
[[r2_buckets]]bucket_name = "screenshots-bucket"binding = "screenshots_bucket"preview_bucket_name = "preview-screenshots-bucket"experimental_remote = true
Running wrangler dev --x-remote-bindings
with the above configuration means that:
- Your Worker code runs locally
- All calls made to
env.screenshots_bucket
will use thepreview-screenshots-bucket
resource, rather than the productionscreenshots-bucket
.
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 experimental_remote: true
in your Wrangler configuration:
To interact with a real headless browser for rendering. There is no current local simulation for Browser Rendering.
{ "browser": { "binding": "MY_BROWSER", "experimental_remote": true },}
[browser]binding = "MY_BROWSER"experimental_remote = true
To utilize actual AI models deployed on Cloudflare's network for inference. There is no current local simulation for Workers AI.
{ "ai": { "binding": "AI", "experimental_remote": true },}
[ai]binding = "AI"experimental_remote = true
To connect to your production Vectorize indexes for accurate vector search and similarity operations. There is no current local simulation for Vectorize.
{ "vectorize": [ { "binding": "MY_VECTORIZE_INDEX", "index_name": "my-prod-index", "experimental_remote": true } ],}
[[vectorize]]binding = "MY_VECTORIZE_INDEX"index_name = "my-prod-index"experimental_remote = true
mTLS:
To verify that the certificate exchange and validation process work as expected. There is no current local simulation for mTLS bindings.
{ "mtls_certificates": [ { "binding": "MY_CLIENT_CERT_FETCHER", "certificate_id": "<YOUR_UPLOADED_CERT_ID>", "experimental_remote": true
} ]
}
[[mtls_certificates]]binding = "MY_CLIENT_CERT_FETCHER"certificate_id = "<YOUR_UPLOADED_CERT_ID>"experimental_remote = true
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.
{ "images": { "binding": "IMAGES" , "experimental_remote": true }}
[images]binding = "IMAGES"experimental_remote = true
Workers for Platforms users can configure experimental_remote: true
in dispatch namespace binding definitions:
{ "dispatch_namespaces": [ { "binding": "DISPATCH_NAMESPACE", "namespace": "testing", "experimental_remote":true } ]}
[[dispatch_namespaces]]binding = "DISPATCH_NAMESPACE"namespace = "testing"experimental_remote = true
This allows you to run your 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.
Certain bindings are not supported for remote connections during local development (experimental_remote: true
). These will always use local simulations or local values.
If experimental_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.
-
Durable Objects: Enabling remote connections for Durable Objects may be supported in the future, but currently will always run locally.
-
Environment Variables (
vars
): 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: 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: Static assets are always served from your local disk during development for speed and direct feedback on changes.
-
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: Local development sessions typically don't contribute data directly to production Analytics Engine.
-
Hyperdrive: This is being actively worked on, but is currently unsupported.
-
Rate Limiting: Local development sessions typically should not share or affect rate limits of your deployed Workers. Rate limiting logic should be tested against local simulations.
-
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.
Wrangler provides programmatic utilities to help tooling authors support remote binding connections when running Workers code with Miniflare.
Key APIs include:
experimental_startRemoteProxySession
: Starts a proxy session that allows interaction with remote bindings.unstable_convertConfigBindingsToStartWorkerBindings
: Utility for converting binding definitions.experimental_maybeStartOrUpdateProxySession
: Convenience function to easily start or update a proxy session.
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.
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 experimental_startRemoteProxySession
. It can be quite convenient to however read the binding declarations with unstable_readConfig
and then
pass them to experimental_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 experimental_startRemoteProxySession
.
This wrapper simplifies proxy session management. It takes:
- The path to your Wrangler config, or an object with remote bindings.
- The current proxy session details (or
null
if none).
It returns:
null
if no proxy session is needed.- An object with the proxy session details if started or updated.
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
experimental_maybeStartOrUpdateRemoteProxySession
).
Here's a basic example of using Miniflare with experimental_maybeStartOrUpdateRemoteProxySession
to provide a local dev session with remote bindings. This example uses a single hardcoded KV binding.
import { Miniflare, MiniflareOptions } from "miniflare";import { experimental_maybeStartOrUpdateRemoteProxySession } from "wrangler";
let mf;
let remoteProxySessionDetails = null;
async function startOrUpdateDevSession() { remoteProxySessionDetails = await experimental_maybeStartOrUpdateRemoteProxySession( { bindings: { MY_KV: { type: "kv_namespace", id: "kv-id", experimental_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()`
import { Miniflare, MiniflareOptions } from "miniflare";import { experimental_maybeStartOrUpdateRemoteProxySession } from "wrangler";
let mf: Miniflare | null;
let remoteProxySessionDetails: Awaited<ReturnType<typeof experimental_maybeStartOrUpdateRemoteProxySession>
> | null = null;
async function startOrUpdateDevSession() {remoteProxySessionDetails =await experimental_maybeStartOrUpdateRemoteProxySession({bindings: {MY_KV: {type: 'kv_namespace',id: 'kv-id',experimental_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()`
Separate from Miniflare-powered local development, Wrangler also offers a fully remote development mode via wrangler dev --remote
. Remote development is not supported in the Vite plugin.
npx wrangler dev --remote
yarn wrangler dev --remote
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.
- For most development tasks, the most efficient and productive experience will be local development along with 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.
- Iteration is significantly slower than local development due to the upload/deployment step for each change.
- When you run a remote development session using the
--remote
flag, a limit of 50 routes per zone is enforced. Learn more in Workers platform limits.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-