🐛 Bug Report — Runtime APIs: a dynamic import() started in a request never settles if that request ends first (legacy module registry) · Issue #7779 · cloudflare/workerd · GitHub
Skip to content

🐛 Bug Report — Runtime APIs: a dynamic import() started in a request never settles if that request ends first (legacy module registry) #7779

Open

Description

@ethan-leonard

What happens

With the default (legacy) module registry, the promise returned by a dynamic import() started while a request is being handled is tied to that request's IoContext. If the request finishes before the import settles, the promise never settles. A later request that awaits the cached promise hangs until the client gives up. There is no exception and no log line. With the new_module_registry compatibility flag, the same worker works.

Any code that caches an import() promise (at module scope, in a global, or in a lazily built app) and first runs it inside a short request, such as a CORS preflight, is exposed. We hit this on deployed Workers: after a preflight started the import, about a third of fresh isolates stopped answering auth requests. Downstream report of the same wedge: better-auth/better-auth#10315.

Minimal reproduction

worker.js:

let cached;
export default {
  async fetch(request, env, ctx) {
    const { pathname } = new URL(request.url);
    if (pathname === "/start") {
      cached ??= import("node:async_hooks");
      return new Response(null, { status: 204 });
    }
    const m = await cached;
    return new Response("settled: " + typeof m.AsyncLocalStorage);
  },
};

config.capnp:

using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
  services = [ (name = "main", worker = .mainWorker) ],
  sockets = [ ( name = "http", address = "127.0.0.1:8787", http = (), service = "main" ) ]
);
const mainWorker :Workerd.Worker = (
  modules = [ (name = "worker", esModule = embed "worker.js") ],
  compatibilityDate = "2026-07-24",
  compatibilityFlags = ["nodejs_compat"],
);
workerd serve config.capnp --experimental
curl -i http://127.0.0.1:8787/start                  # 204
curl -i --max-time 4 http://127.0.0.1:8787/later     # no response

Use a fresh process for each run, because the first request is the one that matters.

Variant (fresh process) GET /start, then GET /later
nodejs_compat 204, then no response (curl gives up after 4 s; two runs)
nodejs_compat + new_module_registry 204, then 200 settled: function in 4 ms

Tested with workerd 1.20261016.1 and 1.20260811.1 on macOS arm64, and through Miniflare 5.20260811.1-alpha. import("./dep.js") of a second module in the worker hangs the same way (tested through Miniflare).

Where

src/workerd/io/worker.c++, configureDynamicImports (legacy registry only). When an IoContext is current, the dynamic import callback returns context.awaitIo(js, handleDynamicImport(...)). The comment on IoContext::awaitIo in io-context.h says that if the IoContext is canceled, the JS promise never resolves. When no IoContext is current, the same callback resolves synchronously, which is why imports started at startup are fine. The new registry resolves dynamic imports synchronously in the V8 callback, which matches the table above.

Expected

Either the import settles no matter which request started it (as with new_module_registry), or the promise rejects with an error saying the originating request ended. If the current behaviour is intended for the legacy registry, a note in the docs about import() and request lifetime would help, because the symptom shows up in a later request as a silent hang.

Workarounds

Enable new_module_registry; keep the import alive with ctx.waitUntil(cached) in the request that starts it (works in my Miniflare test); or import statically at module scope.

I can work on a patch if you tell me which direction you prefer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions