Cloudflare Workers Gets Closer To Real Node Module Semantics
Cloudflare rebuilt the Workers module registry around URL-based resolution, lazy compilation, shared code caches, import.meta support, and better Node.js compatibility.

Cloudflare rebuilt the module registry inside workerd, the open-source runtime that powers Workers. The practical change is that Workers is moving closer to the way Node.js and browsers actually resolve, load, and cache modules.
That matters because Workers now has Node.js compatibility enabled by default for new compatibility dates, supports larger Worker applications up to 64 MiB on all plans, and is trying to run more real npm-shaped server code at the edge. API compatibility is only half the job. Module semantics decide whether an application behaves the same after it leaves a local Node process.
What Changed
The new Workers module registry is available behind the new_module_registry compatibility flag. Cloudflare says it changes the runtime behavior in several concrete ways:
import.meta.url,import.meta.main, andimport.meta.resolve()work.- Module specifiers are parsed and resolved as URLs, including query strings and fragments.
node:built-ins resolve to the same module instance across import paths.- Import attributes are validated instead of silently ignored.
require()of ES modules follows Node.jsrequire(esm)rules.- Errors are more consistent across static import, dynamic import, and
require()paths. - Modules compile lazily when they are first imported.
- Code caches can be shared across isolates running the same Worker version.
The existing registry is not being removed today. Existing deployed Workers keep running as before unless the project opts into the new behavior.
Why We’re Paying Attention
Small teams like Workers because deployment is simple, but the hard cases usually show up when a dependency expects normal Node module behavior. A package may call import.meta.resolve(). A build may leave query strings on imports. A CommonJS path may require() an ES module. A framework may depend on consistent module identity for a built-in like node:buffer.
Those are not glamorous features, but they are the difference between “it bundled” and “it behaves.” The more Workers can preserve normal module behavior, the less glue code developers need in their build pipeline.
The other useful part is operational. Cloudflare’s older registry compiled all modules up front and kept private copies per V8 isolate. The new registry compiles modules lazily and can share code caches across isolates. For large serverless applications, startup time and memory behavior are not abstract runtime details. They decide whether a dependency-heavy route feels cheap enough to run at the edge.
How It Works
The biggest design shift is treating module specifiers as URLs instead of filesystem-style paths. That lets Workers line up with browser module identity rules and Node’s URL-aware module behavior.
For example, query strings and fragments become part of the module identity:
// counter.js
let n = 0;
export function increment() {
return ++n;
}
import { increment as incA } from "./counter.js?a";
import { increment as incB } from "./counter.js?b";
incA(); // 1
incA(); // 2
incB(); // 1, a separate module instance
That behavior can surprise people coming from bundler output, but it matches the web platform model: same source path plus a different query string is a different module specifier.
The require(esm) behavior is another compatibility point worth testing. If an ES module exports a string-named module.exports, Node lets that module control what CommonJS require() receives. Cloudflare says the new registry follows that rule, while still rejecting ES module graphs that use top-level await because require() has to return synchronously.
A Small Useful Test
This post does not need a sample repo. The useful test is a small Worker compatibility probe that teams can add to an existing project before changing production behavior.
In a test Worker, opt into the new registry:
{
"compatibility_date": "2026-09-10",
"compatibility_flags": ["new_module_registry"]
}
Then exercise the pieces most likely to expose hidden assumptions:
import { Buffer } from "node:buffer";
import data from "./config.json" with { type: "json" };
export default {
async fetch() {
const resolved = import.meta.resolve("./config.json");
return Response.json({
main: import.meta.main,
url: import.meta.url,
resolved,
bufferWorks: Buffer.from("edge").toString("base64"),
configName: data.name,
});
},
};
Run it locally with Wrangler, then test the dependencies that made you nervous: dynamic imports, mixed CommonJS and ESM packages, JSON imports, Wasm modules, and any framework code that reaches for import.meta.
For an existing production Worker, the safer rollout is boring:
Workers module-registry rollout
1. Enable the flag in a branch or preview environment.
2. Run the route set that covers SSR, API handlers, scheduled jobs, and dynamic imports.
3. Compare cold-start behavior and runtime errors against the current registry.
4. Look for import-attribute, top-level-await, and module-not-found errors.
5. Promote one low-risk Worker first, then repeat with larger applications.
Cost And Operational Notes
The feature itself is a compatibility flag, not a new paid service. The surrounding Workers limits still matter.
Cloudflare’s Workers limits page now lists a 64 MiB Worker size limit for both Free and Paid plans, with Node.js compatibility enabled by default for compatibility dates of August 4, 2026 or later. Free-plan Workers still have tighter request and CPU limits, including 100,000 requests per day and 10 ms CPU time per HTTP request. Paid Workers have higher request capacity and can raise CPU time for heavier work.
That means the new registry makes larger Node-shaped deployments more realistic, but it does not turn Workers into a general VM. Keep an eye on startup time, memory use, package size, and unsupported Node APIs. The runtime can support many stable Node APIs, but some APIs remain partial or only make sense in a serverless context.
The other operations note is compatibility drift. A Worker that depends on old bundler transforms may behave differently when the runtime starts preserving more module semantics. That is a good reason to test with real application routes instead of only deploying a tiny hello-world Worker.
What We’d Watch Next
The important follow-up is whether the flag becomes the default after enough compatibility data lands. If it does, edge deployments of regular Node frameworks should need fewer special cases.
We would also watch how bundlers react. The Cloudflare post points out that Vite 8 with Rolldown can emit an entry module plus chunks. A more capable runtime registry gives bundlers permission to do less flattening and preserve more of the module graph. That could make edge debugging easier because runtime behavior will look less like a giant generated file and more like the source application developers wrote.
The short version: if your Workers project pulls in serious npm dependencies, this is worth testing early. The win is not a shiny API. It is fewer weird module surprises at deploy time.