---
title: "05 — Wrangler, Miniflare, and the Local Dev Loop"
uid: cloudflare-development-tools
tags: ["tooling", "ci-cd", "cloudflare", "developer-experience", "roadmap:cloudflare", "wrangler", "miniflare"]
excerpt: "Wrangler owns the whole lifecycle; Miniflare runs the exact `workerd` runtime locally. So 'works on my machine' actually means 'works in production' here."
date: 2026-08-13T03:28:18+0000
source: https://www.aveshina.my.id/en/blog/cloudflare-development-tools
---

"Just deploy it and pray" was my Workers deployment ritual, until the local tooling turned out to be the same runtime as production. The model that changed it: **the entire development lifecycle is owned by one CLI, Wrangler, and the local simulator it drives (Miniflare) runs the exact same workerd runtime that production does — so "works on my machine" actually means "works in production" here.** [1][2] That tightness is the whole reason the dev loop feels different from every other serverless platform I've used.

The framing that landed is the symmetry. In most serverless setups, the local emulator is a different runtime than production — different quirks, different APIs, behaviors that pass locally and fail deployed. With Workers, Wrangler runs Miniflare, Miniflare runs workerd, and workerd _is_ the production runtime, open-sourced [3]. The thing my code executes against on localhost:8787 is the same binary my code executes against on the edge. The dev loop is short because there's nothing to translate.

```figure
<svg viewBox="0 0 740 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The Workers dev loop. Left: wrangler dev drives Miniflare locally, running workerd — the same runtime as production. Center: code iteration happens against localhost. Right: wrangler deploy pushes the same code to the global edge. Below: CI/CD pipelines run the same wrangler deploy step on push.">
  <defs>
    <marker id="warrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- local box -->
    <rect x="30" y="50" width="280" height="140" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="170" y="74" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">local — wrangler dev</text>

    <rect x="50" y="90" width="110" height="40" rx="6" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="105" y="114" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">Wrangler CLI</text>
    <rect x="180" y="90" width="110" height="40" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="235" y="108" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">Miniflare</text>
    <text x="235" y="122" font-size="9" fill="#500724" text-anchor="middle">→ workerd runtime</text>

    <path d="M160,110 L178,110" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#warrow)"/>

    <text x="170" y="160" font-size="10" font-style="italic" fill="#1e1b4b" text-anchor="middle">localhost:8787 — same runtime as prod</text>
    <text x="170" y="178" font-size="9" fill="#475569" text-anchor="middle">fast iteration, hot reload, DevTools</text>

    <!-- arrow to deploy -->
    <path d="M310,120 L420,120" fill="none" stroke="#16a34a" stroke-width="2" marker-end="url(#warrow)"/>
    <text x="365" y="112" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">wrangler deploy</text>

    <!-- production -->
    <rect x="425" y="50" width="280" height="140" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="565" y="74" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">production — global edge</text>

    <rect x="475" y="95" width="180" height="40" rx="6" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="565" y="119" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">same workerd, 300+ cities</text>

    <text x="565" y="160" font-size="10" font-style="italic" fill="#052e16" text-anchor="middle">no runtime translation between the two</text>
    <text x="565" y="178" font-size="9" fill="#475569" text-anchor="middle">works locally ⇒ works deployed</text>

    <!-- CI/CD band -->
    <rect x="30" y="210" width="675" height="0" rx="0" fill="none"/>
    <text x="370" y="222" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">CI/CD pipelines just run the same `wrangler deploy` on every push — no extra tooling</text>
  </g>
</svg>
```

## Wrangler: the one CLI

Wrangler owns the entire lifecycle, and almost everything I do with Workers goes through it [1]. The commands I use constantly:

- wrangler init — scaffold a new project (TypeScript or JavaScript, with sensible defaults).
- wrangler dev — run the Worker locally against Miniflare, with hot reload.
- wrangler deploy — push to the edge.
- wrangler tail — stream real-time logs from a deployed Worker.
- wrangler secret put / wrangler kv key put — manage secrets and KV data without putting them in code.
- wrangler d1 execute, wrangler r2 object put — interact with the storage products from the command line.

Configuration lives in wrangler.toml (or .json/.js), which declares the Worker's name, entry point, compatibility date, and all its bindings — KV namespaces, R2 buckets, D1 databases, service bindings. The config file _is_ the source of truth for what the Worker can talk to [1]. Wrangler reads it for local dev and ships it on deploy.

## Miniflare: the local runtime

Miniflare is the local simulator, and the key fact about it is that it's not an approximation — it runs workerd, the actual open-source Workers runtime [2][3]. That means every binding, every runtime API, every quirk I might depend on behaves locally the way it behaves in production.

The features I lean on:

- **Faithful binding simulation.** KV namespaces, Durable Objects, R2 buckets, Queues, D1 databases, Cache API — all work locally against an in-memory or on-disk stand-in. I can develop a stateful Worker end-to-end without deploying.
- **Hot reload.** Code changes are picked up on save, the Worker restarts in milliseconds, and I see the result on the next request.
- **Source maps and stack traces.** Errors point at the actual line in my source, not the bundled output.
- **Deterministic testing.** Because the local runtime is faithful, integration tests written against Miniflare have a high correlation with production behavior.

Miniflare is now built into Wrangler 3+, so I don't usually invoke it directly — wrangler dev drives it for me. It's worth knowing it exists because it explains _why_ local development is so reliable here.

## DevTools and debugging

The debugging story has two layers [4]:

- **Local DevTools.** wrangler dev exposes a Chrome DevTools endpoint. I can open chrome://inspect, attach to the Worker, set breakpoints, inspect variables, step through execution — the full browser-debugging experience, applied to server code.
- **Production observability.** On the edge I don't have a debugger, but I have wrangler tail for live logs, Cloudflare's dashboard analytics for metrics, and source maps that make deployed stack traces readable.

The habit that pays off: develop against wrangler dev with DevTools until the logic is solid, then deploy and rely on structured console.log output streamed through wrangler tail for the inevitable production-only issues (real traffic patterns, real upstream APIs, real edge locations).

## CI/CD pipelines

The thing that surprised me is how little specialized tooling CI/CD needs. A Workers deployment pipeline is, at its core, four lines in a GitHub Action [5]:

```
- uses: actions/checkout@v4
- uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    command: deploy
```

That's it. Wrangler handles the rest — bundling, the upload, the routing. GitLab CI, CircleCI, Travis CI all work the same way: install Wrangler, run wrangler deploy with a token [5]. The patterns worth adding on top are the same as any pipeline: lint and test before deploy, gate production deploys behind a manual approval, use environments for staging-vs-production config, and store the API token as a CI secret — never in code.

The decision of _when_ to deploy is the interesting part, not the mechanics. Trunk-based teams deploy on every push to main; release-based teams deploy on tags. Workers' fast, atomic deploys (and instant rollbacks via wrangler rollback) make trunk-based deploy-on-push realistic in a way it isn't on slower platforms.

## How I use this

The dev loop I've settled on: wrangler dev running in a terminal, DevTools attached, a wrangler.toml checked into the repo with all bindings declared, secrets in wrangler secret, and a GitHub Action that runs wrangler deploy on every merge to main. Rollback is one command if something breaks. The whole loop — edit, save, see the result locally, push, have it live on the edge — is measured in seconds, and that speed is the thing that actually shapes how confidently I can ship.

## References

[1] Cloudflare, "Wrangler documentation," Cloudflare Workers Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/wrangler/](https://developers.cloudflare.com/workers/wrangler/)

[2] Cloudflare, "Miniflare - Cloudflare Workers," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/testing/miniflare/](https://developers.cloudflare.com/workers/testing/miniflare/)

[3] Cloudflare, "Introducing workerd: the open source Workers runtime," Cloudflare Blog, 2022. [Online]. Available: [https://blog.cloudflare.com/workerd-open-source-workers-runtime/](https://blog.cloudflare.com/workerd-open-source-workers-runtime/)

[4] Cloudflare, "Debugging Cloudflare Workers," Cloudflare Workers Playground Docs. [Online]. Available: [https://developers.cloudflare.com/workers/playground/#devtools](https://developers.cloudflare.com/workers/playground/#devtools)

[5] GitHub, "GitHub Actions — Automate your workflow," 2024. [Online]. Available: [https://github.com/features/actions](https://github.com/features/actions)

```quiz
Q: What does `wrangler dev` actually run your Worker against?
- A simplified JavaScript sandbox that approximates the edge
- Miniflare, which runs workerd — the same runtime production uses
- A remote copy of the Worker on a single edge location
correct: 1
explain: Wrangler drives Miniflare, which runs the open-source workerd runtime. Local and production share the same runtime binary, so behaviors translate directly.

Q: Where do Worker bindings (KV, R2, D1, service bindings) get declared?
- Inline in the Worker code via `import`
- In `wrangler.toml` (or .json/.js), which is the source of truth for what the Worker can touch
- In the Cloudflare dashboard only, never in code
correct: 1
explain: Bindings are configuration, declared in wrangler.toml. Wrangler reads it for local dev and ships it on deploy.

Q: What's the minimum viable CI/CD pipeline for a Workers deploy?
- A custom Docker image that builds the Worker and uploads it via the REST API
- Install Wrangler, run `wrangler deploy` with an API token stored as a CI secret
- A webhook that triggers Cloudflare to pull from your Git repo
correct: 1
explain: Wrangler owns the whole lifecycle, so the pipeline is essentially one command. A GitHub Action or GitLab CI job just installs Wrangler and runs deploy with a token.

Q: How do you inspect a deployed Worker's live behavior in production?
- Attach Chrome DevTools directly to the edge isolate
- Use `wrangler tail` to stream real-time logs, plus dashboard analytics and source-mapped stack traces
- You can't — production Workers have no observability
correct: 1
explain: There's no live debugger on the edge, but `wrangler tail` streams logs in real time, and Cloudflare's analytics surface metrics. DevTools debugging happens locally via `wrangler dev`.

Q: Why does "works on my machine" carry more weight on Workers than on most serverless platforms?
- Because the runtime is proprietary and tightly controlled
- Because Miniflare/wrangler dev runs the same workerd runtime that production does — there's no emulator gap
- Because Workers can't run locally at all
correct: 1
explain: The local simulator isn't an approximation; it's the actual production runtime. The behaviors you test locally translate to the edge without an emulator mismatch.
```
