Dynamic worker builds
Dynamic Workers are compiled by a small workerd sidecar whose public RPC accepts source values and returns Worker Loader values. The OS Worker never imports the 13 MiB esbuild Wasm module. There is no container, shell, filesystem checkout, Wrangler subprocess, or project build command in this path.
Thin worker-bundler adapter#
A dynamic-worker source contains exactly one property named after the upstream function:
createWorker or createApp. Its value has the same shape as that function's
options. The one substitution is files: it may point at an inline map or a
project repo snapshot because a custom worker-bundler FileSystem object
cannot cross the service-binding boundary.
On a cache miss OS routes the immutable build key through one keyed
WorkerBuildCoordinatorDurableObject. The elected caller resolves the file
descriptor to Record<string, string> and puts the map back into the same
options object. Paths are not rewritten. OS then makes exactly the call the
source names:
createWorker({ ...source.createWorker, files: resolvedFiles }); orcreateApp({ ...source.createApp, files: resolvedFiles }).
There are no OS rules for the number or names of files. Entry points may be
explicit or left for worker-bundler to detect from wrangler config,
package.json, or its defaults. A package.json is passed through untouched;
worker-bundler attempts to install the root manifest's dependencies from the
selected npm registry before resolving the graph.
Ordinary Worker builds additionally receive these platform-owned virtual modules by exact specifier:
@iterate-com/capnwebiterate/sdk/capnwebiterate/processorsiterate/processors/cloudflareiterate/sdk
The public data options mirror worker-bundler's serializable knobs: bundle,
externals, target, minify, sourcemap, registry, jsx,
jsxImportSource, define, loader, and conditions, plus the relevant
worker or app entry points, createWorker.virtualModules, and
createApp.assets/assetConfig. The library's esbuild-plugin escape hatch,
custom FileSystem objects, and binary ArrayBuffer assets are not exposed
because they cannot cross the public data boundary unchanged.
Seeded app example#
Todo and Guestbook deliberately use only server.tsx and client.tsx, but
that is their example layout, not the build contract. Their refs spell out the
ordinary createApp options:
source: {
createApp: {
bundle: false,
client: "apps/todo/client.tsx",
files: { type: "repo", repoPath: "/repos/config" },
server: "apps/todo/server.tsx",
},
}With bundle: false, worker-bundler transforms the server as separate modules
while still compiling each client entry into a browser bundle. React and the
Iterate browser SDK are ordinary package.json dependencies and imports.
createApp may return any number of client bundles and explicit text assets.
OS caches the returned content, manifest, config, and Wrangler compatibility
settings, then delegates requests to worker-bundler's own
handleAssetRequest before falling through to the server Worker. HTML routing,
redirects, headers, conditional requests, cache policy, and SPA fallbacks
therefore remain worker-bundler behavior rather than an OS imitation. The
platform still injects the Iterate status overlay into eligible HTML
responses.
Cache and failure model#
The build key includes normalized source identity, all build options, compatibility settings, bundler version, artifact schema version, and (for ordinary Workers) the complete generated platform-module contents. Identical requests share cached artifacts across projects. With an unlocked dependency range, the first successful registry resolution becomes that key's cached artifact until expiry.
KV stores one complete modules/assets JSON record per successful key with a 30-day TTL. Build failures are not cached because worker-bundler does not distinguish deterministic source failures from transient registry or runtime failures; a later request tries again.
Expected compiler rejection crosses the build coordinator as a JSON-safe
result ({ ok: false, failure: { kind: "source", message } }). Repo, KV,
sidecar transport, and programming failures still throw. Stateful capability
dispatch keeps the source-failure result as data through its outer Durable
Object hop, then creates the public terminal error locally; this avoids
depending on custom Error properties, which Workers RPC does not preserve.
KV is only the immutable result cache, never a lock: its eventually consistent model has no atomic lease primitive. After the caller's fast KV read misses, every request for the same key reaches the globally named coordinator object. Its leader rechecks KV, reads the repo snapshot, calls the bundler, and writes the artifact once; concurrent followers receive the same plain-data result through promises owned by their own RPC invocations. After success, the coordinator retains the immutable artifact for the rest of that object incarnation. That in-memory tier bridges KV's distributed negative-cache window: later callers whose local KV read still sees the old miss receive the retained plain data without starting another flight, including callers with a zero build budget. Retention never extends the object lifetime and failures are never retained. Different build keys address different objects and therefore build in parallel, following Cloudflare's one coordination atom per Durable Object guidance. A coordinator reset may retry the idempotent build, but normal cross-isolate concurrency cannot create a stampede.
Coordinator telemetry reflects these tiers: worker-build.settled is normally
about once per key per object incarnation, worker-build.reused records later
incarnation-local hits. The retained value is content-addressed immutable build
output, not a last-good or stale artifact; an object reset simply drops it and
resumes through the durable KV cache.
Browser fetches may stop waiting at a small budget while the coordinator's
elected operation continues under waitUntil; callers see the self-refreshing
building page. There is deliberately no last-good artifact, stale serving, KV
lease, or refresh policy.
Actual boundaries#
@cloudflare/worker-bundler 0.2.1 is experimental and runs only in workerd.
Its registry client, package-format support, resolver, esbuild Wasm startup,
CPU/memory limits, and output behavior are the build system's limits. There is
no Vite, Tailwind CLI, TanStack Start adapter, lifecycle-script runner, or
native module toolchain around it.
The current upstream installer has deliberately narrow npm semantics:
- it reads only the root
package.jsonand installsdependenciesplus their recursivedependencies; it does not install rootdevDependencies, peer dependencies, workspaces, or lockfiles; - exact versions, semver ranges,
latest/*, and dist-tags resolve through registry metadata; git, URL, file, and workspace specifications do not; - it runs no lifecycle scripts and exposes a registry URL, not registry auth;
- package tarballs are reduced to a fixed set of text extensions. Native binaries, Wasm payloads, images, fonts, and other binary package files are omitted.
The OS artifact is one JSON KV value, so Cloudflare KV's 25 MiB value limit is a hard upper bound before JSON overhead. Builds and Loader isolates also live under Workers' 128 MiB isolate memory limit, including esbuild Wasm and in-memory source/artifact maps. See the official KV limits and Workers limits.
Today each asset lookup sends the cached text asset map through the bundler service so it can use the upstream handler. That is faithful and simple for small apps, but not a large-asset architecture. Cloudflare's own Dynamic Worker static-assets guidance points toward host-owned KV or R2 asset storage; moving assets there is a follow-up once the package exposes a host-friendly handler boundary.
Warnings are preserved as successful build metadata and logged; OS does not reinterpret them as errors. The JSON artifact cache currently accepts text assets and JSON-safe Worker Loader modules. An ArrayBuffer asset/module is an explicit build failure until the cache grows a binary encoding.