# A Deep Dive into the Workers Runtime’s Rebuilt Module Registry
## Introduction
The core runtime engine powering serverless execution at scale has undergone a significant architectural overhaul. Its module registry — the internal system responsible for resolving, loading, and caching code modules — has been completely rewritten from the ground up. The result is a faster, more robust implementation that closely mirrors the behavior developers expect from Node.js, while maintaining full compatibility with the web’s ESM and WebAssembly module ecosystems.
This article explores what changed, why it matters, and how you can start using the new system today.
—
## How Code Gets Loaded into the Runtime
When you deploy a serverless function to a cloud platform, your source code — spread across many files and dependencies — gets bundled together before upload. The most common approach packs everything into a single large script. Tools like esbuild process relative imports and `require()` calls, inlining them into one monolithic file that can grow to hundreds of thousands of lines. By the time that bundle reaches the runtime, there’s very little of the original module graph left.
Alternative build pipelines take a different approach. Some modern bundlers produce a smaller entry module plus additional chunks created through code splitting, preserving a more realistic module graph at runtime. When deployed without bundling at all, the runtime receives your source files exactly as written, with every import and dependency relationship intact.
Regardless of which path your code takes, something must translate module specifiers into actual executable code, compile it, and hand the engine a module object it can execute. That responsibility falls on the module registry.
—
## Why a Complete Rewrite Was Necessary
The previous implementation had several design limitations that constrained what developers could do and made future evolution difficult.
**Filesystem-style path resolution instead of URL-based resolution.** The old registry treated specifiers as filesystem paths, not URLs. This meant features like `import.meta.url` had no clean implementation, relative imports didn’t follow standard URL resolution rules, and protocol prefixes like `node:` and `cloudflare:` were handled as special-cased string comparisons rather than as proper protocol handlers.
**Eager compilation of entire bundles.** The old system compiled every module in a Worker upfront, regardless of whether it was ever actually imported. Since the platform runs multiple isolated replicas of the same Worker across CPU cores, this meant the identical source code was compiled multiple times and stored in memory separately for each replica — wasting both startup time and memory.
The new registry starts from URLs as the fundamental specifier format. It was designed from day one to embrace laziness (only compiling modules when they are first imported) and cache sharing (compiled modules are shared across all replicas of a Worker, reducing redundant work). The existing registry continues to operate unchanged for Workers already in production, ensuring no disruption to currently deployed code.
—
## Key Features of the New Registry
### import.meta Support
The `import.meta` API gives modules access to information about themselves. Three properties now work correctly:
– **`import.meta.url`** — Returns the module’s resolved URL.
– **`import.meta.main`** — Returns `true` only for the entrypoint module configured for your function, `false` for all other modules.
– **`import.meta.resolve()`** — Resolves a specifier string against the current module’s location without actually importing it. It performs pure string transformation, normalizing percent-encoding the same way `new URL()` does, and throws a `TypeError` if the specifier can’t be parsed as a URL.
For example, `import.meta.resolve(‘./utils.js’)` returns the full URL of that module, while `import.meta.resolve(‘fs’)` recognizes Node.js built-ins and returns their protocol-based specifier.
### Specifiers Are Real URLs
Module specifiers are now parsed and resolved as proper URLs, matching the behavior of browsers and Node.js. This means full URLs can be used directly in import statements, and query strings and fragments create genuinely distinct module instances. A module imported as `./counter.js?a` is treated as a separate instance from `./counter.js?b`, each with its own copy of top-level state and its own `import.meta.url`.
### Import Attribute Validation
Import attributes — the `with { type: ‘json’ }` syntax — are now properly validated according to specification. The `json` attribute type is fully supported. Other recognized types like `text` and `bytes` are rejected with clear error messages indicating they are not yet supported, rather than being silently ignored. Any attribute key other than `type` is treated as a hard error. Additionally, if the specified type doesn’t match the actual module format, a descriptive type mismatch error is thrown.
### CommonJS require() on ES Modules
When you call `require()` on something that turns out to be an ES module, the registry follows Node.js conventions:
– If the module has a string-named export called `’module.exports’`, that value is returned directly.
– Otherwise, the module’s namespace object is returned.
– Built-in modules from the `node:` protocol are a special case — they wrap CommonJS-style APIs in default exports, so requiring them returns the API directly without needing to unwrap a `.default` property.
Modules containing top-level `await` cannot be required synchronously and will throw an error, matching Node.js’s own restriction on requiring async modules.
### Consistent Error Handling
Regardless of whether a module fails to load through a static `import`, a dynamic `import()`, or `require()`, the errors you receive use consistent classes and message shapes. A missing module produces a plain `Error` with a “Module not found” message. An invalid specifier that can’t be parsed as a URL produces a `TypeError`. Circular dependencies surface as plain `Error` objects, never `TypeError`. This consistency makes it easier to build robust error-handling logic on top of dynamic imports.
### Lazy Compilation and WebAssembly Source Phase Imports
Modules are now compiled only when they are first imported, whether that import happens statically or dynamically. This reduces startup overhead, especially for Workers with large dependency trees where not all code is needed for every request.
WebAssembly modules gain support for source phase imports, letting you import the compiled-but-not-instantiated `WebAssembly.Module` object directly using `import source wasmModule from ‘./add.wasm’`, or dynamically via `import.source()`. This gives you direct access to the compiled module, which you can then instantiate manually with your own import object — useful for advanced use cases like caching compiled modules across invocations.
### Node.js Built-in Module Identity
`node:` built-in modules now resolve to the same module instance regardless of how they are reached through the dependency graph. This prevents subtle bugs where the same built-in could have different identities depending on the import path taken.
—
## How to Enable the New Registry
To start using the new module registry, add the `new_module_registry` compatibility flag to your Worker configuration:
“`json
{
“compatibility_flags”: [“new_module_registry”]
}
“`
This flag is not enabled by default yet, so you will need to add it explicitly for both new and existing Workers. The platform is also supporting larger deployment bundles — up to 64 MiB on all plans — and all stable Node.js runtime APIs are now enabled by default.
—
## FAQ
**Q: Will my existing Workers break if I enable this flag?**
A: The vast majority of Workers will work unchanged. The new registry is designed to be a superset of the old behavior, aligning more closely with Node.js and web standards. If you encounter unexpected behavior, it should be treated as a potential regression and reported to the open-source project.
**Q: What happens if I don’t enable the flag?**
A: Nothing changes. Your Worker will continue to use the original module registry implementation, which remains fully supported and operational.
**Q: Can I use both the new registry and the full Node.js API support together?**
A: Yes. The new module registry works alongside the existing Node.js API compatibility layer. In fact, the registry was designed to make Node.js module resolution patterns work correctly in the serverless environment.
**Q: Does the new registry improve cold start times?**
A: Yes, in two ways. First, lazy compilation means modules are only compiled when actually imported, not upfront. Second, cache sharing across V8 isolate replicas means the same compiled module is reused rather than being recompiled for each replica.
**Q: What module types does the registry handle?**
A: The registry handles ECMAScript modules (ESM), CommonJS modules, and WebAssembly modules. Each type follows its own resolution and instantiation rules within the registry.
**Q: Is this change open source?**
A: Yes. The `workerd` runtime is open source, and the new module registry implementation is part of that project. You can find reference documentation and file issues directly in the project repository.
**Q: Can I query what version of the registry a Worker is using?**
A: The presence of the `new_module_registry` compatibility flag in your Worker’s configuration indicates the new registry is active. There is no separate version string exposed at runtime.
—
## Conclusion
The rebuilt module registry represents a fundamental shift in how the serverless runtime handles code organization and module resolution. By adopting URL-based specifiers, embracing lazy compilation, sharing caches across execution replicas, and aligning with Node.js and web standards, the new implementation unlocks larger and more complex applications while reducing overhead. Features like `import.meta.resolve()`, proper import attribute validation, `require(esm)` support, and WebAssembly source phase imports bring the runtime much closer to parity with local development environments.
Developers can try the new registry today by adding a single compatibility flag to their deployment configuration. As the platform continues to evolve, this foundation will make it possible for bundlers and tooling to work even more closely with the runtime, reducing unnecessary transformations and letting the runtime handle module resolution directly.
—
Thank you for reading



