1---
2title: 2.0.0-beta.41 - Worker Runtime Rewrite
3date: 2026-05-20T22:00:00Z
4category: release
5excerpt: A big internal rewrite of the Cloudflare Worker runtime fixes hung requests and dropped responses from RPC and HTTP API workers. The Worker entrypoint is now a thin shell over `WorkerBridge.ts`, scopes are linearized per request, and the same schema-driven pattern (HTTP API, RPC, and Durable Object proxies) is now stable end-to-end.
6---
7
8:::caution
9**Upgrade if you're using `Cloudflare.Worker` with `HttpApi` or
10`RpcServer`.** beta.40 and earlier shared a Worker-level Effect
11runtime across requests, which caused intermittent 500s, hung
12`fetch` calls under load, and "promise resolved on the wrong I/O
13context" errors from workerd. beta.41 scopes everything per
14request — the failure modes documented in
15[#374](https://github.com/alchemy-run/alchemy/pull/374)
16no longer reproduce.
17:::
18
19`v2.0.0-beta.41` is mostly the bridge rewrite. The Worker
20entrypoint is now a thin generated shell that calls into a
21type-checked `WorkerBridge.ts`; scope construction is linear,
22per-request, and no longer mixes promises across the workerd I/O
23boundary. The same fix lands across `Cloudflare.Worker`,
24`Cloudflare.DurableObject`, and `Cloudflare.Workflow`,
25so the schema-driven pattern in the [HTTP API](/cloudflare/apis/effect-http-api)
26and [RPC](/cloudflare/apis/effect-rpc) guides now holds together under
27real traffic.
28
29## RPC and HTTP API — per-request scopes, no more cross-context promises
30
31The Worker runtime had been caching the user's Worker Effect once
32as a module-scope `Promise`, then mixing and matching scopes
33between `WorkerBundle`, `WorkerRuntime`, and `HttpServer` on each
34request. Three things went wrong in production:
35
361. **Too many scopes.** Every layer in the stack created its own
37 `Scope`, and the close order between them wasn't well-defined.
38 Finalizers ran against scopes that had already been torn down,
39 or never ran at all.
402. **Promises crossed the workerd I/O boundary.** A single cached
41 promise was awaited from multiple requests. workerd treats
42 that as "promise resolved on the wrong I/O context" and aborts
43 the request — manifesting as hung `fetch`es and dropped
44 responses.
453. **Global vs request-level scope confusion.** Construction-time
46 layers (provided once) and per-request layers (provided per
47 `fetch`) were being merged in the wrong order, so request-level
48 services occasionally got resolved from the global scope and
49 then captured stale state from a previous request.
50
51beta.41 refactors the entrypoint into
52[`WorkerBridge.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Workers/WorkerBridge.ts).
53The generated entrypoint is now thin — it just instantiates the
54bridge class. Everything else is type-checked TypeScript in the
55package. Scoping is linear: one `Scope.makeUnsafe()` per request,
56provided into the user's Effect, and closed via `ctx.waitUntil`
57when the response finishes streaming. Nothing is cached as a
58promise; the user's Worker Effect is re-evaluated per request
59against the request's own scope.
60
61> Performance note: the rewrite trades a small amount of
62> per-request setup for correctness. The previous "cache the
63> Worker once" path was the source of the bugs, so we've left it
64> off by default. There's a follow-up to identify what's actually
65> safe to cache (anything that doesn't capture a promise) — for
66> now, correctness first.
67
68[#374](https://github.com/alchemy-run/alchemy/pull/374)
69
70### The schema-driven pattern, end to end
71
72The pattern the [HTTP API guide](/cloudflare/apis/effect-http-api) and
73[RPC guide](/cloudflare/apis/effect-rpc) describe — define a schema, build
74a server, wrap it as `{ fetch }`, and optionally proxy through a
75Durable Object — is the same shape regardless of which transport
76you pick. beta.41 is the release where it actually works for all
77three combinations.
78
79**1. Define the schema once.** Same `Task` schema feeds the HTTP
80API endpoint declarations and the RPC procedure declarations:
81
82```typescript
83// src/task.ts
84import * as Schema from "effect/Schema";
85
86export class Task extends Schema.Class<Task>("Task")({
87 id: Schema.String,
88 title: Schema.String,
89 completed: Schema.Boolean,
90}) {}
91
92export class TaskNotFound extends Schema.TaggedClass<TaskNotFound>()(
93 "TaskNotFound",
94 { id: Schema.String },
95) {}
96```
97
98**2a. HTTP API — declare endpoints, return `{ fetch }`.**
99`HttpApiBuilder.layer(TaskApi).pipe(..., HttpRouter.toHttpEffect)`
100is exactly the `HttpEffect` Workers expect:
101
102```typescript
103// src/worker.ts
104export default Cloudflare.Worker(
105 "Worker",
106 { main: import.meta.filename },
107 Effect.gen(function* () {
108 const tasks = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
109
110 const tasksGroup = HttpApiBuilder.group(TaskApi, "Tasks", (h) =>
111 h.handle("getTask", ({ params }) =>
112 tasks.get(params.id).pipe(
113 Effect.flatMap((o) =>
114 o ? decodeTask(o) : Effect.fail(new TaskNotFound({ id: params.id })),
115 ),
116 ),
117 ),
118 );
119
120 return {
121 fetch: HttpApiBuilder.layer(TaskApi).pipe(
122 Layer.provide(tasksGroup),
123 HttpRouter.toHttpEffect,
124 ),
125 };
126 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
127);
128```
129
130**2b. RPC — declare procedures, return `{ fetch }`.** Same
131schemas, same Worker shape, different transport. The RPC server
132also produces an `HttpEffect`:
133
134```typescript
135// src/rpcs.ts
136export class TaskRpcs extends RpcGroup.make(
137 Rpc.make("getTask", {
138 payload: { id: Schema.String },
139 success: Task,
140 error: TaskNotFound,
141 }),
142 Rpc.make("createTask", {
143 payload: { title: Schema.String },
144 success: Task,
145 }),
146) {}
147```
148
149```typescript
150// src/worker.ts
151export default Cloudflare.Worker(
152 "Worker",
153 { main: import.meta.filename },
154 Effect.gen(function* () {
155 const tasks = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
156
157 const handlersLayer = TaskRpcs.toLayer({
158 getTask: ({ id }) => /* ... */,
159 createTask: ({ title }) => /* ... */,
160 });
161
162 return {
163 fetch: RpcServer.toHttpEffect(TaskRpcs).pipe(
164 Effect.provide(handlersLayer),
165 Effect.provide(RpcSerialization.layerJson),
166 ),
167 };
168 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
169);
170```
171
172**3. Bind a Durable Object, get a typed fetcher per instance.**
173The DO's Init returns the same `{ fetch }` shape — an
174`HttpEffect` from either `HttpApiBuilder` or
175`RpcServer.toHttpEffect`. Inside the Worker, bind the DO and use
176`Cloudflare.toHttpClient(stub)` to turn each instance into a
177typed client:
178
179```typescript
180const tasksDO = yield* TasksObject;
181
182const getDOClient = (id: string = "default") =>
183 HttpApiClient.makeWith(TaskDOApi, {
184 baseUrl: "http://localhost",
185 httpClient: Cloudflare.toHttpClient(tasksDO.getByName(id)),
186 });
187```
188
189The `baseUrl` is a placeholder — `toHttpClient` short-circuits
190straight to `tasksDO.getByName(id).fetch`, so the call never
191leaves the isolate. The same wrapper plugs into
192`RpcClient.layerProtocolHttp` if the DO speaks RPC instead:
193
194```typescript
195const makeDOClient = (id: string = "default") =>
196 RpcClient.make(DoRpcs).pipe(
197 Effect.provide(
198 RpcClient.layerProtocolHttp({ url: "http://localhost" }).pipe(
199 Layer.provide(
200 Layer.succeed(
201 HttpClient.HttpClient,
202 Cloudflare.toHttpClient(tasksDO.getByName(id)),
203 ),
204 ),
205 Layer.provide(RpcSerialization.layerNdjson),
206 ),
207 ),
208 );
209```
210
211One `Task` schema. Worker exposes HTTP API or RPC. DO speaks the
212same protocol back to the Worker. Per-DO-instance fetchers via
213`getByName(id)`. With beta.41's per-request scoping, the whole
214chain runs without scope leaks or cross-context promise errors.
215
216The guides walk through the full thing:
217
218- [Effect HTTP API](/cloudflare/apis/effect-http-api) — schema → endpoints
219 → Worker → typed client, plus the DO-backed variant.
220- [Effect RPC](/cloudflare/apis/effect-rpc) — schema → procedures →
221 Worker → typed client, plus streaming and DO-backed RPCs.
222
223## Other fixes
224
225- **`WorkerProps.env` flows into `InferEnv`** — typing
226 `worker.env.MY_VAR` now picks up keys you declared in
227 `env: { ... }` on the Worker, not just bindings
228 ([#351](https://github.com/alchemy-run/alchemy/pull/351)).
229- **`WorkerExecutionContext` + `ExecutionContext` in fetch types** —
230 the Worker fetch handler now exposes both the Effect-side
231 `ExecutionContext` (with the request scope) and the raw
232 workerd `WorkerExecutionContext`, so `ctx.waitUntil(...)` and
233 friends are reachable from inside the handler without an
234 extra cast. Thanks
235 **[Michael K](https://github.com/michaelkleene)**
236 ([#358](https://github.com/alchemy-run/alchemy/pull/358)).
237- **DO methods receive the Worker env** — previously, RPC methods
238 on a `DurableObject` were running with an empty `env`
239 context, breaking any binding the DO needed to call into. Thanks
240 **[Dillon Mulroy](https://github.com/dmmulroy)**
241 ([#369](https://github.com/alchemy-run/alchemy/pull/369)).
242- **`Redacted` survives sidecar RPC serialization** — sidecar
243 was unwrapping `Redacted` values during JSON round-trips,
244 leaking secrets in dev logs. They're now preserved end to end.
245 Thanks **[Juliaan](https://github.com/Juliaan)**
246 ([#356](https://github.com/alchemy-run/alchemy/pull/356)).
247- **`lightningcss` + `fsevents` externalized in the Worker
248 bundler** — both are platform-specific native deps that don't
249 belong in the Worker bundle. Thanks
250 **[Michael K](https://github.com/michaelkleene)**
251 ([#363](https://github.com/alchemy-run/alchemy/pull/363)).
252- **Worker teardown force-deletes scripts** — if a Worker had
253 stuck Durable Object migrations from a previous failed deploy,
254 `destroy` would hang. It now force-deletes the script and
255 proceeds ([#348](https://github.com/alchemy-run/alchemy/pull/348)).
256- **Eager terminal status events while waiting on deps** — the
257 CLI now shows `pending` for resources blocked on dependencies
258 and emits terminal status (created/updated/failed) as soon as
259 the resource's own reconcile finishes, rather than batching at
260 the end. Thanks **[Michael K](https://github.com/michaelkleene)**
261 ([#376](https://github.com/alchemy-run/alchemy/pull/376)).
262
263## Contributors
264
265Big thank-you to everyone who shipped code in this beta:
266
267- **[Michael K](https://github.com/michaelkleene)** — Worker fetch type fix ([#358](https://github.com/alchemy-run/alchemy/pull/358)), `lightningcss`/`fsevents` externalization ([#363](https://github.com/alchemy-run/alchemy/pull/363)), eager terminal status events ([#376](https://github.com/alchemy-run/alchemy/pull/376))
268- **[Dillon Mulroy](https://github.com/dmmulroy)** — DO methods receive Worker env ([#369](https://github.com/alchemy-run/alchemy/pull/369))
269- **[Juliaan](https://github.com/Juliaan)** — `Redacted` across sidecar RPC ([#356](https://github.com/alchemy-run/alchemy/pull/356))
270
271## Where to go next
272
273- [Effect HTTP API guide](/cloudflare/apis/effect-http-api)
274- [Effect RPC guide](/cloudflare/apis/effect-rpc)
275- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta41)
276- [Compare v2.0.0-beta.40 → v2.0.0-beta.41](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.40...v2.0.0-beta.41)