Why Miniflare exists

Miniflare started as a personal project in late 2020. While building a Workers app, the author found wrangler dev's edge-based preview loop too slow for rapid iteration, especially when frontend tooling like Vite offered near-instant reloads. Existing local simulators such as cloudflare-worker-local and cloudworker lacked support for newer Workers features like Workers Sites, so Miniflare 1.0 was built to fill that gap.

The first release, launched in July 2021 on the Cloudflare Workers Discord server, delivered fast reloads, source map support, readable error output, a visual error page, and debugger integration. It quickly became an official Cloudflare project and is now part of the Workers toolchain, integrated into wrangler 2.0.

A redesigned simulator

Workers evolved quickly after Miniflare 1.0 shipped. Durable Objects gained input and output gates, compatibility dates arrived, and JavaScript modules became a supported Worker format. Miniflare 2.0, released today, tracks those changes and was rebuilt around three goals:

  • Modular: Components are now split into separate packages such as @miniflare/kv and @miniflare/durable-objects, so each can be imported independently for testing, and future features like R2 Storage can be added without a monolith.
  • Lightweight: Dependencies dropped from 122 third-party packages totalling 88.3MB to 23 packages and 6MB, by taking advantage of Node.js 16 built-ins.
  • Accurate: The simulator mirrors runtime quirks and thrown errors, so code that breaks in production tends to break locally first.

New in this release are a live-reload mode and a custom Jest test environment. wrangler dev remains the most faithful preview since it runs on the real edge with real data, but Miniflare 2.0 is designed to be close.

Running a local dev server

With wrangler 2.0 integration, the simplest path is npx wrangler@beta dev --local for a Worker or npx wrangler@beta pages dev for Cloudflare Pages Functions. Node.js 16 is required.

For existing setups using Wrangler 1, or where more control is needed, standalone Miniflare is available. Point it at a project with a wrangler.toml and run:

  • npx miniflare --live-reload to start a dev server that hot-reloads; bindings, KV namespaces, Durable Objects, and secrets from .env are picked up automatically.
  • --kv-persist to keep KV data across restarts.
  • npx miniflare --help to list options including multi-worker setups and HTTPS support.
  • Visiting http://localhost:8787/cdn-cgi/mf/scheduled manually triggers a scheduled event handler.

Testing with Jest

Miniflare 2.0 ships a custom Jest test environment that exposes Workers runtime APIs to test code. For a module Worker that counts URL visits in KV—note that KV is eventually consistent, so Durable Objects would be the production choice for counters—unit tests can exercise the handler directly. The project also links to an example repo combining TypeScript, esbuild, Jest, and Durable Objects. Non-Jest setups can write integration tests in vanilla Node or other frameworks; an AVA example is available in the docs.

Under the hood

Miniflare runs on Node.js, which shares V8 with the Workers runtime but exposes different APIs. To keep Node globals away from user code while injecting Workers APIs, Miniflare uses the vm module to execute Workers in an isolated V8 context. Request and Response come from undici, the Node team's native fetch implementation, and service worker addEventListener and event dispatch use Node's built-in EventTarget.

The essential logic boils down to:

import vm from "vm";
import { Request, Response } from "undici";

// An instance of this class will become the global scope of our Worker,
// extending EventTarget for addEventListener and dispatchEvent
class ServiceWorkerGlobalScope extends EventTarget {
  constructor() {
    super();

    // Add Worker runtime APIs
    this.Request = Request;
    this.Response = Response;

    // Make sure this is bound correctly when EventTarget methods are called
    this.addEventListener = this.addEventListener.bind(this);
    this.removeEventListener = this.removeEventListener.bind(this);
    this.dispatchEvent = this.dispatchEvent.bind(this);
  }
}

// An instance of this class will be passed as the event parameter to "fetch"
// event listeners
class FetchEvent extends Event {
  constructor(type, init) {
    super(type);
    this.request = init.request;
  }

  respondWith(response) {
    this.response = response;
  }
}

// Create a V8 context to run user code in
const globalScope = new ServiceWorkerGlobalScope();
const context = vm.createContext(globalScope);

// Example user worker code, this could be loaded from the file system
const workerCode = `
addEventListener("fetch", (event) => {
  event.respondWith(new Response("Hello mini-miniflare!"));
})
`;
const script = new vm.Script(workerCode);

// Run the user's code, registering the "fetch" event listener
script.runInContext(context);

// Create an example request, this could come from an incoming HTTP request
const request = new Request("http://localhost:8787/");
const event = new FetchEvent("fetch", { request });

// Dispatch the event and log the response
globalScope.dispatchEvent(event);
console.log(await event.response.text()); // Hello mini-miniflare!

Rather than wiring every API by hand, Miniflare 2.0 organizes features as plugins. Each package exports globals and bindings for the sandbox, and options are annotated with types, CLI flags, and mappings to Wrangler config keys.

Durable Objects and input gates

Older Durable Objects code relied on explicit transaction() calls to stay consistent. Miniflare 1.0 implemented that with optimistic-concurrency control. With input and output gates, the runtime now defers events while storage operations are in flight, removing the need for manual transactions.

Input gates require two methods: one to close the gate while a storage operation runs, and one to wait until the gate reopens. Each Durable Object gets its own InputGate; storage operations wrap themselves in runWithClosed, and event delivery checks in with waitForOpen before proceeding.

The tricky part is context. There is one global scope for a Worker and all its Durable Objects, so a single per-object gate can't be wired into a global fetch without threading it through every call. Node's async_hooks module, specifically AsyncLocalStorage, provides the answer: it stores the current gate in a context that follows async flows automatically.

blockConcurrencyWhile(closure) maps directly to runWithClosed(), but that creates a deadlock risk. If a closure itself calls fetch on the same object, the fetch waits for the gate to open while the closure is holding it closed. Making InputGates nestable resolves this: the outer gate stays closed so external fetches defer, while an inner open gate lets the closure's own fetch complete. The full gates implementation is in the repo for reference.

HTMLRewriter and async handlers

HTMLRewriter parses and transforms HTML streams. Cloudflare's edge runtime binds it to the Rust lol-html library via C; Miniflare instead uses WebAssembly bindings for the same library, which work in Node.js.

Those bindings, though, assume synchronous handlers. Rewriting code that awaits external resources—say, fetching data inside an element handler—needs an async callback, and the Rust closure receiving it is synchronous. Waiting on a promise there is like trying to await in a non-async function.

Binaryen's Asyncify feature solves this. When a handler calls await_promise, Asyncify unwinds the WebAssembly stack to temporary storage; JavaScript then awaits the promise and rewinds the stack to resume rewriting exactly where it paused. The implementation lives in the html-rewriter-wasm package.

// jest.config.js
const { defaults } = require("jest-config");

module.exports = {
  testEnvironment: "miniflare", // ✨
  // Tell Jest to look for tests in .mjs files too
  testMatch: [
    "**/__tests__/**/*.?(m)[jt]s?(x)",
    "**/?(*.)+(spec|test).?(m)[tj]s?(x)",
  ],
  moduleFileExtensions: ["mjs", ...defaults.moduleFileExtensions],
};

What's Next for Miniflare

Miniflare 2.0 is now bundled with Wrangler 2.0, and Cloudflare encourages developers to try it out and send feedback.

The Cloudflare Workers team, along with contributors who have filed issues, suggested improvements, or participated in the Discord community, have played a key role in developing this release.

The project's creator notes that with this release, they might finally be able to complete their original Workers side project.