Cloudflare rewrote the module registry in workerd, the open-source component that serves as the core of the Workers runtime, to improve speed and standards compliance and bring module loading and resolution closer to Node.js behavior. This coincides with enabling stable Node.js APIs by default in Workers and allowing applications up to 64 MiB to be deployed on all plans, after removing the compressed bundle-size limit.
However, the most important change is not merely the number of available APIs. Node.js applications also depend on how the runtime identifies, loads, and caches modules. This includes ESM, CommonJS, and WebAssembly modules—responsibilities handled by the module registry within workerd.
What changed in the new registry?
Developers can try the new implementation by adding the new_module_registry flag to the Worker settings. When enabled, Workers supports import.meta.url, import.meta.main, and import.meta.resolve(), and treats module specifiers as real URLs, including query strings and URL fragments.
In practice, this means that relative imports follow the same rules as new URL(), and complete URLs can be used as module specifiers. Modules that differ in their query string or URL fragment also become separate modules, even if they refer to the same source. Thus, two versions of the same file can have separate top-level state, while reusing the same specifier returns the same module instance.
The registry also correctly validates import attributes. json is currently available, while types such as text and bytes are defined but rejected with a clear message because they have not yet been enabled. Unknown attributes are also no longer silently ignored; instead, they produce an explicit error.
Greater compatibility with Node.js
When loading an ESM module, require() follows Node.js rules for require(esm). If the module exports a value named module.exports, that value is returned; otherwise, the call returns the module namespace object. The exception is the node: built-ins included in workerd, which return the expected CommonJS interface instead of forcing developers to access a default property.
There is an important limitation: require() cannot load a module that contains, or depends on a module that contains, top-level await, because require must return the result synchronously. In this case, Workers throws an error, and asynchronous import() is the appropriate path. This rule remains in effect even if the module itself was previously loaded through import().
The new version also standardizes error classes and message wording regardless of the loading method, whether through a static import, dynamic import(), or require(). Failure to find a module returns an ordinary error, while a specifier that cannot be parsed as a URL produces a TypeError. This consistency benefits developers building custom loaders or retry logic.
What changes in practice for developers?
Workers tools such as Wrangler used to bundle most application files and dependencies into a single module using esbuild, reducing the size of the graph handled by the runtime. When using the Cloudflare Vite plugin, Vite 8 relies on Rolldown to produce an entry module and additional modules when code is split, such as dynamically loaded modules.
The new registry gives bundling tools more room to perform fewer transformations and rely more heavily on the runtime to resolve modules. This is particularly important when deploying applications as multiple modules, using the --no-bundle option, or handling Wasm files, text, and binary files as separate files rather than embedding them in a single bundle.
The new implementation also defers compilation until a module is first imported, whether through a static or dynamic import, and enables code-cache sharing among multiple V8 isolate instances running the same Worker. According to Cloudflare, this addresses some of the repeated compilation and multiple copies of source code in memory seen in the previous implementation.
Limitations and points to note
Despite these changes, the new registry will not be enabled automatically for any Worker, old or new, regardless of the compatibility date in use. The flag must be added explicitly. Cloudflare has also kept the previous implementation in use and says that deployed Workers will continue to operate as before.
The new implementation also supports source-phase imports for WebAssembly modules, returning a WebAssembly.Module object directly. However, this feature currently works only with WebAssembly; any other type results in a syntax error. Therefore, the update is not an unrestricted, across-the-board transition for all module-loading paths, but rather a more compatible foundation that developers can test incrementally.
Editorial reading: The change’s actual value lies in moving Workers from a partial simulation of module behavior toward a model closer to JavaScript and Node.js standards. This may reduce the transformations imposed by build tools and make multi-module applications more portable. Its ultimate impact, however, will depend on developers testing the new flag, especially with dependencies that use require(), top-level await, or import attributes. The fact that it is not enabled by default also means that improved compatibility is not yet the general behavior for all applications.