Skip to content

Build1 publisher3 min readPublished

Cloudflare gates workerd's rewritten module registry behind a new_module_registry flag

The Node.js API surface and the larger bundle ceiling arrive by default, but the resolution fixes that unblock ported apps only apply to Workers that opt in, and they bite hardest once your bundler stops flattening imports.

The Engineer · Build desk

Illustration accompanying Cloudflare gates workerd's rewritten module registry behind a new_module_registry flag

What happened

  • Cloudflare has rewritten the module registry in workerd, the open-source core of the Workers runtime, describing it as faster, more standards-compliant and more closely aligned with Node.js.
  • The new behaviour is opt-in per Worker, switched on by adding new_module_registry to the compatibility_flags array in the Worker's configuration.
  • With the Cloudflare Vite plugin, Vite 8 bundles through Rolldown and emits an entry module plus code-split chunks, so workerd receives a build-generated module graph.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • capability Ports that died on resolution rather than on a missing API now have a route in. A dependency calling import.meta.resolve() was previously a hard stop, and that class of failure is what the flag addresses.
  • constraint Size planning changes shape rather than disappearing, because 64 MiB still binds while how well the bundle compresses no longer decides whether it fits.
  • decision Teams shipping one flattened esbuild script get little from the flag alone, so the real choice is whether to move the bundler to a graph-emitting configuration at the same time.
  • exposure If bundlers take Cloudflare's invitation to transform less, resolution correctness moves from your build machine into the runtime, where a bad specifier surfaces after deploy rather than during a build.

Start with what esbuild leaves behind. Wrangler's default bundle inlines relative imports and `require()` calls into a single module script, rewriting the `import` and `require` statements into ordinary function calls [13]. Cloudflare says those scripts have grown to as many as multiple hundreds of thousands of lines [14], and that by the time the file reaches workerd there usually is not much of a module graph left [15]. Only specifiers the build keeps intact ever reach the resolver. The rewrite is worth what your build leaves for it to do.

Some specifiers do survive. Node.js APIs you import resolve to modules built into workerd rather than polyfills inlined in your code, and Wasm, text and binary modules are uploaded as separate files referenced by specifier [18]. Under the Cloudflare Vite plugin, Vite 8 bundles with Rolldown, which converts CommonJS to ESM where needed and emits an entry module plus code-split chunks, so the runtime receives a build-generated graph instead of a flattened file [19]. Those paths are where `new_module_registry` [4] actually binds [21].

The fix targets resolution failures, a distinct class from missing APIs. Cloudflare's own example is `import.meta.resolve()`: uploading multiple modules of multiple types has been possible for years, but the runtime did not resolve them the way other runtimes do, so a dependency calling that function failed [16][17]. With the flag set, `import.meta.url`, `import.meta.main` and `import.meta.resolve()` work [5], specifiers parse as real URLs including query strings and fragments [6], import attributes such as `with { type: 'json' }` are validated [8], `require()` on an ES module follows Node's `require(esm)` rules [9], and errors use consistent classes and messages whichever loading path triggered them [10].

Two entries in that list are about identity and timing rather than syntax. `node:` built-ins now resolve to the same module instance however you reach them [7], which is the property that keeps module-level state inside those built-ins to one copy [23]. Modules compile lazily on first import, static or dynamic [11], so compile work tracks the modules a request actually reaches and a branch never imported is never compiled [24].

The default-on half of the announcement is the API surface and the size limit. Cloudflare says the runtime now supports every stable Node.js API "that you might want to use in a serverless context", enabled by default [2]; the qualifier is the specification, and the set is whatever Cloudflare judged you would want. On size, the figure is 64 MiB on all plans with the separate compressed-bundle limit removed [3], so the ceiling is still 64 MiB and the thing that stopped mattering is how well your bundle compresses [22].

For the flag to change a deployment, two things have to hold: the app was failing on module resolution rather than on a missing API, and the build hands workerd a graph rather than one inlined script. Cloudflare also describes the new registry as faster, and that speed claim comes without a published measurement in this material [25].

What to watch

  • Whether new_module_registry stops being opt-in and becomes the behaviour at a future compatibility date.
  • Whether Rolldown ships options that skip CommonJS-to-ESM conversion and hand resolution to workerd instead.
  • Any published timing behind Cloudflare's claim that the rewritten registry is faster.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories