The response came back with a field I never typed: cacheStatus: null, tucked into the cached block of a KV read. It arrived one morning in April through the ordinary env object from a disposable server on my laptop. No Cloudflare account was involved.
A coding agent iterating on a Worker deploys and dispatches far more often than I would by hand, and it pays every wait on every pass. I want my loop, the agent’s loop, and CI to stay on the laptop. The same requirement has been pulling the rest of my tools towards agent-speed iteration.
That April morning I’d deployed two Workers into the server. The first was a small downstream service that echoes back whatever you send it. The second was the one I cared about: an app Worker declaring four bindings by config, the way wrangler.toml would declare them.
bindings: {
DB: { type: "d1", name: "DB", resourceId: "main-db" },
CACHE: { type: "kv", name: "CACHE", resourceId: "main-kv" },
FILES: { type: "r2", name: "FILES", resourceId: "main-bucket" },
DOWNSTREAM: { type: "service", name: "DOWNSTREAM", resourceId: "downstream" },
}
I built the server to answer one question: can Worker code use its bindings through the normal env object, unchanged, with no local-only adapter anywhere in the codebase? So the app Worker does exactly what production Worker code does:
await env.DB.exec("CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, name TEXT)");
await env.DB.prepare("INSERT INTO events (name) VALUES (?)").bind("launch").run();
const rows = await env.DB.prepare("SELECT id, name FROM events ORDER BY id").all();
await env.CACHE.put("latest", "launch", { metadata: { kind: "event" } });
const cached = await env.CACHE.getWithMetadata("latest");
await env.FILES.put("brief.txt", "worker bindings survived local dispatch");
const file = await env.FILES.get("brief.txt");
const service = await (await env.DOWNSTREAM.fetch("/echo?source=brief")).json();
Then I dispatched a request into it. The response, verbatim:
{
"status": 200,
"body": {
"rows": [
{
"id": 1,
"name": "launch"
}
],
"cached": {
"value": "launch",
"metadata": {
"kind": "event"
},
"cacheStatus": null
},
"file": {
"text": "worker bindings survived local dispatch",
"size": 39
},
"service": {
"service": "downstream",
"source": "brief"
}
}
}
R2 returned the 39 bytes I put in, and the service response came from the downstream Worker without the request leaving the process. The result is narrow: the binding surface exists and behaves as those four calls expect. The KV read’s cached block contains the field I never typed. Nothing in the Worker code mentions cache status, so this is the real getWithMetadata shape showing through. A mock would have contained only the fields I remembered, and the transcript shows that I missed one.
The disposable server is open-cloud, my self-hosted TypeScript replica of the Cloudflare developer platform. It implements Workers, D1, KV, R2, Durable Objects, Queues, Vectorize, AI Gateway, and more on Bun 1.2+, SQLite, and the local filesystem. It contains fewer than 10,000 lines, and a fresh server starts in under 100 ms.
The mock I didn’t write
The project has no mocks. Local and CI runs exercise services with the real binding shapes. If production uses env.DB.prepare(...) while the test harness imports another helper, an agent has two contracts to reason about and may apply the wrong one. CI faces the same problem. With real binding objects, the agent deploys the Worker, binds resources from configuration, dispatches a request, and inspects state and response. Whatever it ships to Cloudflare has already run against the same env calls.
“Inspect the state” is literal. State lives in plain SQLite files under data/, with the per-service layout documented in implementation.md. An insert can be checked with sqlite3 against the database file, and rm -rf data/ restores a clean slate between runs.
What’s behind the bindings
wrangler dev and Miniflare already cover much of this, and they win on runtime fidelity because they run the actual workerd engine. wrangler dev has been local-first since 2023, so most iteration stays on the machine. There is still a wait. The development server takes seconds to boot, @cloudflare/vitest-pool-workers re-imports each test module into workerd over WebSocket RPC, and unsupported bindings still reach the cloud. I wanted one process that accepts programmatic deployment and dispatch all day without leaving the laptop. A deployment is one server.deployWorker({...}) call, and /__admin serves health checks and Prometheus metrics. The process also provides local implementations for Vectorize, AI Gateway, Browser Rendering, and other services Miniflare does not simulate.
The services use ordinary local storage. D1 maps the prepare, bind, run, and all methods to SQLite through bun:sqlite. KV keeps namespace data and metadata in SQLite. R2 writes bytes and metadata into a filesystem-backed bucket. Each service binding is a synthetic { fetch } object that captures the target Worker’s ID and routes back into the runtime. The app Worker’s env object is assembled once at deployment from binding configurations and resource IDs. Shared services are cached by {type}:{resourceId}, so a second Worker bound to main-db reads the same rows written by the first.
open-cloud targets Cloudflare’s API shape, not bit-for-bit behavior parity. Region routing, propagation delays, proprietary internal rate limits, and the full production control plane are out of scope. If your bug lives there, you need Cloudflare.
At idle, the server uses about 80 MB of RSS, and in-process dispatch takes around 45 µs. By default there is no isolation, and Workers are imported directly into the Bun process. For less trusted code, isolate mode gives each Worker its own thread and heap at about 280 µs per dispatch. Both modes are cheap enough to leave running. If you have a Worker available, deploy it into a server of your own and inspect the response. The file body I stored in R2 says “worker bindings survived local dispatch.” I typed that before running the request, which was optimistic. All 39 bytes came back.
Update, June 2026: Cloudflare shipped wrangler deploy --temporary, which keeps an agent’s loop in their cloud on a 60-minute lease. Mine stays on the laptop.