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 }); or
  • createApp({ ...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/capnweb
  • iterate/sdk/capnweb
  • iterate/processors
  • iterate/processors/cloudflare
  • iterate/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.json and installs dependencies plus their recursive dependencies; it does not install root devDependencies, 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.

Was this page helpful?