🐛 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
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
🐛 Bug Report — Runtime APIs: a dynamic import() started in a request never settles if that request ends first (legacy module registry) #7779
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.
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