feat(runtime-sdk): add lifespan="worker" to asgi.fetch for a worker-lifetime lifespan - #239
Draft
whitphx wants to merge 1 commit into
Draft
feat(runtime-sdk): add lifespan="worker" to asgi.fetch for a worker-lifetime lifespan#239whitphx wants to merge 1 commit into
lifespan="worker" to asgi.fetch for a worker-lifetime lifespan#239whitphx wants to merge 1 commit into
Conversation
whitphx
force-pushed
the
feat/asgi-worker-lifespan
branch
2 times, most recently
from
August 30, 2026 13:02
c260f0f to
3f53f17
Compare
whitphx
marked this pull request as draft
August 30, 2026 13:44
whitphx
force-pushed
the
feat/asgi-worker-lifespan
branch
from
August 30, 2026 13:48
3f53f17 to
f293b5b
Compare
fetch() runs a full lifespan startup and shutdown around every request, so an app whose lifespan owns state that outlives a request loses it each time. Add fetch(..., lifespan="worker"), which starts the app once per isolate, shares one startup across concurrent cold-start requests, and retries if that startup failed. Shutdown is left to isolate teardown. The default is unchanged.
whitphx
force-pushed
the
feat/asgi-worker-lifespan
branch
from
August 30, 2026 14:20
f293b5b to
70b56f9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
asgi.fetchruns a full lifespan startup and shutdown around every request. That is right for a stateless app, but an app whose lifespan owns state meant to outlive a request (a connection pool, a warmed cache, a model loaded once, the session state behind a WebSocket) rebuilds it on each call and tears it down again.This adds an opt-in mode, with the default unchanged:
"worker"starts the app's lifespan once per isolate and leaves it running, so the state the lifespan populates reaches every later request throughscope["state"], which each request still receives as its own shallow copy. Concurrent cold-start requests share one startup rather than each driving its own, and a failed startup is dropped so the next request retries instead of one transient error becoming permanent.Discussions
Shutdown. There is no isolate-teardown hook provided from CF Workers, so
lifespan.shutdownis never sent.Waiting on a startup another request began. A request arriving during cold start waits on a future created in the first request's IoContext, and workerd drops that wake-up once the first request has ended (
handle_cross_request_promise_resolution). The future stays cached unresolved, so every later request waits on it too and the app never starts in that isolate. There is no exposure after startup completes, since the state is cached as a plain value from then on.Disabling the compat flag would bring back the bugs it prevents, so
_ensure_worker_lifespanbounds the wait instead and forgets the cached future on expiry, turning a dead isolate into one failed request and a retry.Is a deadline the right shape, or is there a better handoff between contexts?
WORKER_LIFESPAN_STARTUP_TIMEOUTdefaults to 30s, which is a guess: too low re-drives a slow cold start and starts the app twice.API shape. The mode is a keyword on
fetch, so the SDK caches the lifespan in a module-level dict keyed by the app. An explicit holder would put that lifetime in the caller's hands instead:That drops the SDK-side globals, makes "once per isolate" visible in the user's own code rather than implied, and reaches
asgi.entrypoint, which hardcodesfetch(...)and so cannot opt into the keyword form at all. The cost is new public API surface. I went with the keyword because it is the smaller change, but the holder shape may be the better one to live with.Test Plan
(run from
packages/runtime-sdk)The in-worker harness runs everything inside one long-lived request, so two things it cannot show: a response body truncated by a missing
waitUntil, and the bounded wait above doing its job (workerd cancels the hung sub-request on its own before the deadline matters). The streaming test covers the mode working rather than those regressions, and the timeout is unverified by the suite.