News

Cloudflare rewrites Workers module registry for Node.js compatibility

Cloudflare has rebuilt the module registry in workerd, its Workers runtime, to be faster, more standards-compliant, and more closely aligned with Node.js. The new implementation treats module specifiers as URLs, supports import.meta APIs, and enables lazy compilation and cache sharing.

September 9, 2026· 3 min read· Source: Cloudflare Blog
Cloudflare rewrites Workers module registry for Node.js compatibility

Cloudflare has rewritten the module registry in workerd, the open-source core of its Workers runtime, to be faster, more standards-compliant, and more closely aligned with Node.js. The new implementation, available via the new_module_registry compatibility flag, changes how modules are resolved, loaded, and cached.

Why a new registry?

The old registry resolved module specifiers as filesystem-style paths, not URLs. That made it hard to implement import.meta.url, and relative imports didn't follow the same rules as new URL(). Protocols like node: and cloudflare: were handled as special-cased string prefixes rather than real protocols.

The old registry also compiled the entire Worker bundle up front, even if some modules were never imported, and kept a separate private copy of everything per V8 isolate. Since Cloudflare runs multiple V8 isolates of the same Worker to spread load, that meant compiling the same source multiple times and holding multiple copies in memory.

The new registry starts from URLs as the specifier format and treats laziness and cache sharing as design goals from day one. The existing registry isn't going away—deployed Workers will continue to work as before.

What changes with the new flag

Enabling new_module_registry brings several improvements:

  • import.meta.url, import.meta.main, and import.meta.resolve() all work.
  • Module specifiers are parsed and resolved as real URLs, including query strings and fragments.
  • node: built-ins resolve to the same module instance no matter how you reach them.
  • Import attributes (with { type: 'json' }) are correctly validated.
  • require() on an ES module follows Node.js' require(esm) rules.
  • Errors use consistent classes and messages regardless of which loading path triggered them.
  • Modules compile lazily when first imported.
  • WebAssembly modules support source phase imports.

import.meta in action

With the new registry, import.meta.url gives you the module's URL, and import.meta.main is true only for the entrypoint module. import.meta.resolve() resolves a specifier against the current module without importing it—it's a pure string transform, same as in Node.js and browsers.

One detail: import.meta.resolve() normalizes percent-encoding like new URL(), so it collapses paths like ./a/../b.js, but doesn't decode characters that were already percent-encoded.

What this means for bundlers

The new registry opens the door for bundlers like Rolldown (used by Vite 8) to perform fewer transformations and rely more on the runtime for module resolution. When you import a Node.js API in a Worker, you're importing a module built into workerd, not a polyfill bundled into your code. Wasm, text, and binary modules are provided as separate files, referenced by specifier rather than inlined.

If you deploy with --no-bundle or upload multiple modules directly, the full module graph shows up at runtime exactly as written. In all cases, the module registry's job is to take a specifier, work out what code it points to, compile it, and hand V8 a module object it can link and run.

The new registry starts from URLs as the specifier format and treats laziness and cache sharing as things to design in from day one.
Manul X Editorial