Guy Bedford
90d · built 2026-09-30
Performance
What Guy Bedford shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+0.1engineers
delivers like 1.1 (1.1x pre-AI)
Output (ETV)
8.2ETV
+222.8% vs 2.5 prior
Features share
42.0%
+1.4 pp vs prior window
Fixes share
24.0%
+24.0 pp vs prior window
Work mix
42% Features8.4% Maintenance22.6% Tests3% Docs24% Fixes
32 commits over 90 days, ending 2026-09-30.
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| workerd | 23 | 6.8 |
| workers-rs | 6 | 1.4 |
Most impactful commits
Top 10 by ETV in the last 90 days.
- 1.7ETVnode:net: add net.Server and cloudflare:node connectHandler (#7306) * server: bind listening sockets before starting services Sockets are resolved and bound up front and handed to listenOnSockets() already bound, so the port a socket actually got (for a configured port of 0) is known while workers are being created. The CONNECT authority handed to a TCP socket's connect() handler is built from the bound endpoint, so it is truthful for port 0. * io: per-IoContext port scope key; sockets: expose scope key and inbound listeners IoContext gains an identity object for JS state scoped to it, and Worker::Api reports the inbound socket listeners configured to deliver to the worker (workerd fills these from the bound sockets targeting the worker's service). cloudflare-internal:sockets exposes both: getPortScopeKey() returns the current Durable Object's key or undefined, and getInboundListeners() the declared listeners as bound. * sockets: half-open inbound sockets and settle closed after client half-close An inbound socket delivered to a connect() handler is now created with allowHalfOpen, so the handler can still reply after the peer has finished sending; previously the write side was force-closed on the peer's FIN, which breaks any send-then-shutdown client. And maybeCloseWriteSide now resolves `closed` on its early-return path: when the writable was already closed by the time the readable hit EOF, both sides are done but `closed` never settled. The test lives in its own self-bound worker since connect-handler-test.js is also embedded by tail-worker-test, which asserts its exact trace stream. * node:net: end the readable on close via push(null) and detach on reconnect onConnectionClosed emitted 'end' directly, which could duplicate the read loop's own EOF once `closed` settles promptly; push(null) is idempotent, with read(0) so 'end' still fires on a socket nobody reads. The read loop and closed continuation are bound to the handle they started on, and connect() on a live socket detaches the old handle before closing it, so its EOF no longer ends the reconnected socket (and the Duplex auto-end no longer closes the new connection's writable). * cloudflare-internal:http: scope port tables by Durable Object and seed declared listeners A Durable Object instance gets its own port table for its lifetime and binds ports as a separate host would; everything else in the isolate shares one table, so a server is reachable from every request. The isolate table is seeded with the ports the platform delivers inbound connections on: binding port 0 takes an unclaimed declared port when any are declared, and a declared entry outlives its binders. Owners capture the table they bound in and release through it, since release may run in another request's context or in the finalization backstop with none. Inbound routing looks in the current Durable Object's table before the isolate's. http.Server allocates an ephemeral port for 0, never a declared one, since it is reached through httpServerHandler. Each table also owns a synthetic host address from 240.1.0.0/16 (a reserved, never-routed block; Hyperdrive's synthetic hosts use 240.0.0.0/16), and 240.1.255.254 is the gateway that stands for peers the platform does not identify, so a connected or accepted socket never reports the unspecified address as its own or its peer's. * node:net: add net.Server listen(port | options | boundSocket) reserves the port in the scope's port table and installs a connect handler there; listen(boundSocket) adopts the reservation, giving the bind(2) / listen(2) split directly. Where the platform declares the ports it delivers connections on, only those may be listened on (EADDRNOTAVAIL otherwise) and listen(0) takes the first unclaimed one; inside a Durable Object the port is chosen freely. An in-use port is reported via 'error' as in Node; pipes and foreign handles are rejected; a listen() before a previous close() has drained cancels the pending 'close'. Inbound platform sockets routed to the handler are wrapped as net.Sockets with the platform-owned local endpoint (family unknown for a hostname label), and the handler resolves when the connection closes so the inbound request lives that long. reusePort listeners share connections round-robin. Also 'connection' / 'listening' / 'close' / 'drop', address(), close() waiting for connections, maxConnections, getConnections(), pauseOnConnect, keepAliveInitialDelay validation, ref()/unref() no-ops and Symbol.asyncDispose. * cloudflare:node: add connectHandler for inbound net.Server sockets The connect() analogue of httpServerHandler: routes an inbound platform socket to the net.Server listening on the port the socket arrived on, taken from its declared local address (the CONNECT authority for a service binding or Durable Object stub, the bound listener address for a sockets entry), so socket.localPort === server.address().port by construction. handleAsNodeConnection is the direct form. Lookups, for http too, see the current Durable Object's table before the isolate's. The returned promise resolves when the connection closes. * types: regenerate snapshot for cloudflare:node connectHandlergithub.com-cloudflare-workerd · 17e4f5a7 · 2026-09-12
- 1.3ETVworker-build --emscripten (#1061) * worker-build --emscripten Build Workers for wasm32-unknown-emscripten through a worker-build provisioned toolchain: the pinned emsdk release is installed into the cache directory and the frontend patched with backports the Rust link needs (marker-based -sWASM_BINDGEN, NODERAWSOCKETS DNS). rustc drives emcc as the linker for a bin target, emcc runs wasm-bindgen post-link, and the output is collected into the standard build/index.js shape. The worker crate's start function becomes private, since its public init export collided with Emscripten's own. * Keep DWARF in emscripten builds The exnref translation failure on debug builds was wasm-bindgen 0.2.128's --keep-debug output (walrus 0.27.1 .debug_loc), fixed on main by #5328, not binaryen on the linked module. The CLI floor already covers it. * Skip reinit on emscripten and support minified emcc output The worker crate no longer registers the abort reinit hook on wasm32-unknown-emscripten, so the released wasm-bindgen CLI builds --emscripten --release; only debuginfo builds still need #5328. Export and wasm import detection now handle emcc's minified release output. * worker-build: leave every cloudflare: module external when bundling The runtime serves the whole cloudflare: namespace (cloudflare:node, cloudflare:test, ...), not only the three that were listed, and a wasm-bindgen module import of one of them must reach the output as an import rather than fail to resolve in esbuild. * Reentrant JSPI toolchain Backport the JSPI lifecycle hooks, REENTRANT_JSPI fiber stacks and epoll listener API to the 6.0.9 frontend, and take Binaryen from a release carrying the jspi-hooks pass (WebAssembly/binaryen#9102). The release sysroot stamp is dropped after patching so emcc installs the new headers. * emscripten-tcp: note reentrant activations * Use wasm-bindgen JSPI lifecycle hooks integration wasm-bindgen/wasm-bindgen#5333 instruments jspi exports and suspending imports with emscripten's fiber hooks, so under -sREENTRANT_JSPI each fetch activation runs on its own stack. The example takes wasm-bindgen from the submodule until it is released. * ci: build the emscripten example with the submodule wasm-bindgen * ci: accept any HTTP/1.x status line in the emscripten smoke test * Fiber-owned Tokio context under REENTRANT_JSPI Pass --cfg tokio_jspi_hooks so Tokio's emscripten port registers the JSPI lifecycle hooks, and move the example to that Tokio branch. Concurrent fetch activations each drive their own runtime. * worker-build --emscripten: derive exported classes from DurableObject The runtime exposes RPC on a Durable Object class only when it derives from cloudflare:workers' DurableObject; the wasm-bindgen classes are plain, so splice its prototype in. * JSPI exports for event and Durable Object handlers on emscripten On wasm32-unknown-emscripten the handler wrappers are #[wasm_bindgen(jspi)] exports suspending on the handler future, so each activation is its own fiber. The wrappers are left for rustc to expand: expanding both cfg variants in the proc macro deduplicated their export descriptors. * worker-build --tokio: event-loop and JSPI Tokio integration `--emscripten --tokio` schedules each handler on a Tokio event-loop runtime per invocation (tokio-rs/tokio#8479), whose wait is the host event loop, so plain async handlers use tokio::net and tokio::time with no stack switching. `--emscripten --tokio=jspi` is the previous behaviour: JSPI exports on their own fibers blocking on a runtime. The mode reaches the macros as cfg(worker_tokio = ...). The epoll listener backport now delivers readiness through a macrotask: workerd drains microtasks synchronously inside builtin module loads, so a microtask delivery ran one runtime's drive under another's connect(). * Update Cargo.lock for wasm-bindgen-futures tokio feature * Regenerate emscripten patches from the current upstream PR heads reentrant-jspi.patch now carries #27698, #27699 and #27547 at their heads (listener keepalive holds, teardown wakes, macrotask delivery) and noderawsockets-dns.patch carries #27693 at its head (node:dns only, no virtual /etc/hosts), both as exact diffs against the 6.0.9 tree. * Regenerate emscripten patches from the rebased upstream stack epoll-listeners.patch carries #27547 at 34e2cc057 and #27720 (timeout keepalive release), noderawsockets-dns.patch carries #27693 as rebased onto that, and the JSPI hooks / REENTRANT_JSPI backport (#27698, #27699) moves to a trailing jspi-hooks.patch that only --tokio=jspi depends on. * Track epoll listener PR heads: MINIMAL_RUNTIME guard around maybeExit * Record the rebaselined epoll listener PR heads * Track the epoll listener PR heads: native_sigs.py ordering * Drop the JSPI Tokio mode: --tokio is the host-driven event loop The event-loop integration never needed stack switching, so --tokio=jspi, its lifecycle-hooks backport (jspi-hooks.patch), the -sJSPI link, the Binaryen release carrying the jspi-hooks pass, the tokio_jspi_hooks cfg and the JSPI export variant in worker-macros go. --tokio is a plain flag and the emsdk release supplies the whole backend. The noderawsockets-dns patch stays: without stack switching getaddrinfo returns EAI_AGAIN for hostnames, a clean io::Error instead of the host's uncaught "Invalid IP address" from net.connect. * Move --tokio onto tokio#8484 host-driven event loops tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop / LocalEventLoop; the example takes tokio from its branch and mio from its emscripten branch (tokio-rs/mio#1969). The wasm-bindgen submodule moves to the rebuilt emscripten stack: main plus the reinit fix (#5332), legalized export names and the tokio attribute (#5334) adapted to LocalEventLoop, without the emscripten JSPI support. worker's schedule_isolated use is unchanged. * Restore the wasm-bindgen path patches for the submodule stack 0.8.6 dropped them to release on stable wasm-bindgen; worker's tokio feature on wasm-bindgen-futures and #[wasm_bindgen(tokio)] come from the submodule until they ship. * Export the connect handler through async_export_mod Brings #[event(connect)] (#1041) in line with the other handlers so it runs on the Tokio event loop under --tokio. * Track the simplified epoll listeners and tokio#8484 runtime keepalive emscripten#27547 now delivers through unref'd handles with no listener keepalive of its own, and #27720 / #27693 are regenerated from their merged main commits. The example's tokio moves to 5ef5ab3d, where the event loop holds the runtime keepalive itself. * Move onto tokio#8484 hosted event loops The example's tokio moves to 067f92b6, where the emscripten event loop is built hosted: it schedules its own drives and holds the runtime keepalive while it has tasks. The wasm-bindgen submodule follows with the tokio attribute built on it; worker's use of schedule_isolated is unchanged. * Track the emscripten socket stack heads Regenerates the 6.0.9 backports from the current pull request heads: the epoll listeners are unchanged in content, the DNS patch gains emscripten_dns_lookup_async / emscripten_dns_lookup_result (#27742), and a new accept-blocking.patch carries the part of #27724 that reaches the single-threaded Node build, recv surfacing a pending socket error ahead of EOF. * Use #[wasm_bindgen(tokio = "isolated")] for event-loop exports Handlers are plain async exports; worker-build --tokio only adds the attribute option, and wasm-bindgen's own glue bridges the isolated event loop to the returned Promise. Removes worker::__tokio_promise and the Durable Object static_self escape, since wasm-bindgen holds the borrow across an async method. * Assert handler futures unwind-safe for panic = "unwind" async exports * Make the worker `tokio` feature the event-loop switch Replaces `worker-build --tokio` and the injected `worker_tokio` cfg: the `tokio` feature on `worker` enables `worker-macros/tokio`, which emits `tokio = "isolated"` on handler exports, and `wasm-bindgen-futures/tokio`. worker-build passes `--cfg tokio_unstable` for every emscripten build. * Split the emscripten examples: std::fs, Tokio, then TCP emscripten is a word-frequency counter over files on the in-memory filesystem with no Tokio; emscripten-tokio shows spawn, timers, timeout, mpsc, Mutex and join! in plain handlers under the tokio feature; emscripten-tcp becomes a small TCP proxy (POST body or HEAD request to an address, from the Worker or a Durable Object). CI builds and smokes all three. * Move to Emscripten 6.0.10 and the tagged Tokio stack worker-build installs emsdk 6.0.10, which ships the wasm-bindgen marker link, timeout keepalive release and NODERAWSOCKETS DNS; the remaining patches are #27547 and #27742 as carried by guybedford/emscripten 6.0.10-cf.emscripten. The examples pin the guybedford tokio 1.53.1-cf.emscripten and mio 1.2.3-cf.emscripten tags, and the plain emscripten example documents that without Tokio none of the patches are needed. wasm-bindgen renamed the export attribute to experimental_tokio and gates it behind --cfg wasm_bindgen_unstable_tokio; worker-build passes that and tokio_unstable only when the worker tokio feature resolves, so a Worker without it builds against stock Tokio. The new examples are also excluded from the workspace, which the previous commit missed. * Point wasm-bindgen at gbedford/emscripten-stack 2988e0efe wasm-bindgen main plus #5334 with the tagged Tokio and mio pins. The Examples job skips the three emscripten examples, which the dedicated job builds. * Follow the sealed wasm-bindgen and Tokio tips wasm-bindgen moves to gbedford/emscripten-stack ea4368ac0 (main plus #5333) and the examples' lock to the 1.53.1-cf.emscripten tag's current commit. Neither changes behaviour here: the JSPI layers are gated behind cfgs worker-build does not pass. * Point wasm-bindgen at main and pass the crate paths on the impl Main carries #5333 as merged, at the released 0.2.128 version, so the workspace and the CLI worker-build downloads stay aligned until 0.2.129 ships. With #5333's fix the Durable Object impl attribute can name the wasm_bindgen_futures and js_sys paths directly instead of importing them into scope. * udpate readmes * fixup * Resolve hostnames in the TCP example The Tokio patchset tag now routes ToSocketAddrs through Emscripten's asynchronous getaddrinfo (#27742, in the worker-build patches), so the example is the plain TcpStream::connect((host, 80)) HEAD probe again and takes hostnames from the Worker and the Durable Object; CI smokes both. * Bring the emscripten examples into the workspace as experimental_tokio The three examples are ordinary workspace members: the Emscripten patchset (Tokio, mio, libc, wasm-streams) joins the root patch block alongside the wasm-bindgen paths, and the examples keep only their dependencies. The worker feature is experimental_tokio, matching the wasm-bindgen attribute it emits, and is inert off emscripten so the workspace builds and lints for the host. Each README opens with the clone-and-run steps. * Keep the emscripten examples standalone with the Tokio patchset in place Each example's Cargo.toml carries the required Tokio, mio and libc patches as the reference for a Worker's own manifest; the wasm-bindgen path patches are the workspace's alone, so the examples take the crates from crates.io. The worker feature is experimental_tokio. * Move to wasm-bindgen 0.2.129 The workspace and submodule follow the release; the examples resolve it from crates.io and worker-build downloads its CLI, whose --keep-debug output debuginfo builds need. * Durable Object connect handlers and the emscripten-incoming-tcp example Stub::connect opens a TCP connection to an object, served by the new DurableObject::connect handler that #[durable_object(connect)] exports; Socket::handle_as_node_connection hands an inbound socket to the Node-style server listening on its port. Under experimental_tokio the object's handlers share the thread's ambient event loop, a Durable Object being one I/O context, while event handlers keep a runtime per invocation. The example runs a Tokio TcpListener line-echo server inside an object, with the Worker's connect handler piping inbound connections to it. * Regenerate the epoll listeners patch from the moved emscripten tag emscripten#27547 now schedules a host turn per wake instead of coalescing onto a pending immediate, which a host may drop: Workers cancels the immediates left over when a request settles, and the listener stayed silent thereafter. Inbound connections after the first now reach the object's listener. * Expose Storage::sync and the raw state and storage objects storage.sync() waits for issued writes to commit; State::as_raw and Storage::as_raw hand the underlying JavaScript objects to libraries that take them directly, such as a filesystem mounted on the object's storage. * Serve inbound TCP with a per-connection runtime off a shared listener The final Emscripten and Tokio tags deliver epoll readiness and hosted drives as microtasks, so a drive runs in the request whose event woke it. The incoming-tcp example is then a plain Worker: the connect export runs on the shared event loop, which hosts only the TcpListener and its accept loop, and each accepted connection is moved onto an event loop of its own inside the connect invocation that delivered it. The epoll listeners patch follows the tag. * Reject failed connect handlers, align example compat dates, take the accept-queue emscripten tag A connect handler returning Err now rejects the export promise instead of panicking, closing only that connection. The test for it is skipped until the bundled workerd carries cloudflare/workerd#7518. Examples move to compatibility_date 2026-09-02 with new_module_registry; emscripten-tcp drops its Durable Object probe. The epoll-listeners patch picks up the NODERAWSOCKETS accept queue of depth one from the tag. CI waits on wrangler's ready line rather than a fetch for connect-only Workers, and carries a disabled cold-burst check for the pending tokio sync-drive fix. * readme updates * Document that a connect handler's return closes the socket * worker-build: single Binaryen core on macOS Binaryen's worker threads overflow their stacks on large modules there.github.com-cloudflare-workers-rs · 20faa6d5 · 2026-09-25
- 0.7ETVnode:net: add net.BoundSocket (#7299) * node:net: add net.BoundSocket Implements the net.BoundSocket API from Node.js 26.4 (nodejs/node#63951, nodejs/node#64375). Workers cannot bind a local endpoint, so the bound address is validated and recorded, then surfaced as the adopting Socket's localAddress/localPort/localFamily, without being applied to the underlying transport. A port of 0 is reported as 0 since no ephemeral port is reserved. * node:net: brand-check BoundSocket adoption instead of instanceof * node:net: virtual local port table shared by net and http Local ports are allocated and conflict-checked in a per-isolate table: BoundSocket reserves its port (ephemeral for port 0, EADDRINUSE on conflict, reusePort sharing), net.Socket autobinds a local endpoint on connect and honors localAddress/localPort, and http.Server.listen() allocates from the same table instead of its private random mapper. Ports are released on close/destroy. BoundSocket adoption uses a private-field brand check rather than instanceof. * node:net: do not record autobound ports in the port table A socket whose request ends before it is destroyed never runs _destroy, so a recorded autobind would be held for the isolate's lifetime and eventually exhaust the ephemeral range. Autobound sockets now take an ephemeral label that skips reserved ports without being recorded; only explicit reservations (BoundSocket, localAddress/localPort, http.Server.listen) enter the table. A reconnect on a live socket drops the previous local endpoint so localPort may be given again, and BoundSocket rejects the path option rather than binding TCP. * node:net: factor the port table into PortTable and fold in portMapper The per-protocol table lives in cloudflare-internal:http so that cloudflare:node keeps loading without nodejs_compat; net wraps conflicts into EADDRINUSE. Entries carry the inbound routing handler that portMapper held, so http.Server.listen() is bind + setHandler and httpServerHandler looks up tcpPorts.getHandler(port). The listen host, when given, is reported in the EADDRINUSE message. * node:net: FinalizationRegistry backstop for port reservations Every explicit reservation (BoundSocket, adopted or explicitly bound Socket, http.Server) registers its owner with the port table so a stranded owner releases its entry on collection; owners unregister before their deterministic release so a reusePort share is never decremented twice. The http handler is held weakly by the table so the server itself can be collected. FinalizationRegistry is only present on compat dates with enable_weak_ref, so the registry is optional and the net and http-server tests enable the flag. * node:net: reserve a port only for a concrete localPort localAddress alone, localPort 0, and null values are not port claims: libraries commonly spread { localAddress: undefined | null } through connect options, and localPort 0 means ephemeral in Node. The address is still used as the reported label. * node:http: hold the listening server strongly in the port table A server the user keeps no reference to must stay routable, as in Node where the libuv handle is the root; the table entry is that root here. Only sockets register with the finalization backstop. The net and http-server tests no longer enable enable_weak_ref, so the default variant exercises the no-registry path and @all-compat-flags the registry path.github.com-cloudflare-workerd · f51abb75 · 2026-09-10
- 0.5ETVnode:fs: open without O_CREAT is ENOENT for a missing path (#7393) * node:fs: open without O_CREAT is ENOENT for a missing path openSync always asked the VFS to create a missing file, so 'r', 'r+' and numeric flags without O_CREAT opened (and left behind) an empty file instead of failing. Rust's File::open, and anything using open(2) to probe for a file, then read zero bytes rather than seeing ENOENT. Threads a create option from the flags through the internal filesystem open to VirtualFileSystem::openFd, which now only creates when asked and otherwise returns a new FsError::NOT_FOUND, mapped to ENOENT with the path. O_CREAT|O_EXCL is checked against create rather than write, matching open(2) for read-only exclusive creates. The stream constructors never passed their flags or mode to fs.open, and WriteStream defaulted to 'r', so streams only opened files because 'r' used to create them. They now pass flags and mode, with WriteStream defaulting to 'w' as in Node. Two existing tests relied on the old behaviour: one opened a missing file for writing with the default flags, and one piped from a never-created file, whose sink also never called next(). * node:fs: open does not create intermediate directories openFd resolves the parent before opening, so a missing parent is ENOENT and a non-directory parent is ENOTDIR instead of the parent chain being created as a side effect of O_CREAT. Drops the positional OpenOptions default argument from openFd; the only caller passes options explicitly. * node:fs: route synchronous fs.open errors to the stream callback fs.open validates flags synchronously, so an invalid stream flag threw inside the lazy-fs promise handler and the stream never emitted error.github.com-cloudflare-workerd · 74cd70f5 · 2026-09-16
- 0.3ETVAdd wasm_memory_discard compatibility flag (#6999) Gates the experimental WebAssembly memory.discard proposal, exposing WebAssembly.Memory.prototype.discard(byteOffset, byteLength) and the memory.discard opcode on a per-worker basis. The feature is wired through V8's per-context conditional-feature mechanism: a per-isolate enabled callback (SetWasmMemoryDiscardEnabledCallback) reports the flag state, which V8 consults both when installing the JS API (InstallConditionalFeatures) and when compiling wasm modules that use the memory.discard opcode. Lock::installWasmMemoryDiscard() enables the feature once per context, based on the flag, via NewContextOptions. Floats the memory.discard V8 patch (0039) into workerd's V8 patch set so the feature is present in workerd's own V8 build. The patch routes the JS API, the jitted memory.discard wrapper and the interpreter through BackingStore::DiscardWasmMemory rather than discarding raw addresses, so page release happens at a single point, and that point discards through the page allocator the reservation actually came from. Under V8_ENABLE_SANDBOX wasm memory is reserved from the isolate group's backing-store allocator, not the platform allocator, so reaching for GetPlatformPageAllocator() decommitted through an allocator that does not own the region -- benign on Linux, where DiscardSystemPages is madvise(MADV_DONTNEED) keyed on address, but wrong on the DecommitPages/SetPermissions path used elsewhere. Also removes the obsolete jspiEnabledCallback declaration left behind when JSPI was stabilized.github.com-cloudflare-workerd · bd568a93 · 2026-08-17
- 0.3ETVRoute all zlib consumers through an autogated router (#7132) The unprefixed zlib symbol names (deflate, inflate, crc32, ...) are now owned by a compiled routing layer, src/workerd/util/zlib-router.c++, which forwards each call to one of the two implementations linked into the binary: chromium zlib (restored under its chromeconf Cr_z_* names as @zlib//:zlib_impl) or zlib-rs (exported under zlib_rs_* prefixes by src/rust/zlib-rs, no longer exporting the unprefixed names). The branch is a process-global atomic set from the compression-rs autogate whenever the gate state changes. Since every consumer is compiled with CHROMIUM_ZLIB_NO_CHROMECONF via the public @zlib target, the gate now universally covers node:zlib, web CompressionStream, crypto crc32, kj-gzip/kj-http (fetch and WebSocket compression), and V8's compression utils. This makes the per-stream ZlibBackend dispatch table redundant, so it is removed along with the zlib-rs bridge TU: compression.{h,c++}, zlib-util.c++, and crypto.c++ are back to plain zlib calls. The bench shim now reaches zlib-rs through the zlib_rs_* symbols, so the Native arm measures real chromium zlib again (~20% deflate / ~15% inflate delta on the streaming workload). A new kj_test verifies the router selects the matching implementation on both sides of the gate.github.com-cloudflare-workerd · d7389205 · 2026-08-26
- 0.3ETVAccept WebAssembly.Module values in the dynamic worker loader (#6997) A module in the worker loader modules bag may now be provided as a WebAssembly.Module directly (e.g. obtained via a source phase import), either as the module value itself or as { wasm: module }. The compiled code is shared with the loaded worker via v8::CompiledWasmModule rather than recompiling the wire bytes, in both the legacy and new module registries. Inside the loaded worker the module can be imported with import source, matching the ESM phase imports proposal semantics. Includes jsg support for unwrapping v8::WasmModuleObject handles so that jsg::V8Ref<v8::WasmModuleObject> can be used as a parameter and struct field type, with rtti and TypeScript type updates.github.com-cloudflare-workerd · 9cdf3805 · 2026-08-21
- 0.3ETVFix two GC strong-root leaks: tail event cf cycles and abandoned pipe writes (#7017) * api: trace cf objects from tail Request wrappers for GC TraceItem::FetchEventInfo::Request held Detail::cf in an untraced strong root while getCf() returns that same mutable object. A tail handler that made cf reference the request (or anything reaching its wrapper) closed an uncollectable JS<->C++ cycle: cf root -> cf -> Request wrapper -> C++ Request -> Detail -> cf root, leaking one object group per mutated tail event until isolate death. Add visitForGc to Request tracing detail->cf. Detail is shared only between the redacted/unredacted Request pair, both of this class, so every holder re-traces the handle each GC cycle. The regression test self-tails, mutates event.request.cf to reference the request, and asserts WeakRefs to both objects are reclaimed; without the fix all 16 remain alive. * api: bound pipe write continuations to the IoContext The two Rc<Pipe::State> captures on the destination write promise were the only continuations in the pipe machinery not routed through ioContext.addFunctor. If the IoContext was torn down while a sink write was in flight, the awaitIo completion was destroyed without running, the Write queue entry never settled, and the bare reaction retained: write promise -> Rc<State> -> owner ref -> destination wrapper -> controller queue -> Write resolver -> write promise. Both stream wrappers plus any buffered chunks stayed on the isolate heap until isolate death, once per abandoned pipe (e.g. client disconnect mid-pipe). addFunctor's IoOwn drops the captures at IoContext teardown, matching every sibling continuation.github.com-cloudflare-workerd · e25810ac · 2026-08-17
- 0.3ETVAdd zlib-rs as an autogated backend for node:zlib (#7109) * Add zlib-rs as an autogated backend for zlib compression streams Backs the shared ZlibStream core with zlib-rs (libz-rs-sys), the memory-safe Rust implementation of the zlib C API, selected per stream by the new compression-rs autogate. This covers both node:zlib and the web CompressionStream/DecompressionStream API. The native chromium zlib remains the default. The z_stream ABI is identical between the two implementations, so the integration is a function dispatch table (ZlibDispatch) consulted at stream construction; node's dictionary/params escape hatches route through the same table via ZlibStream::dispatch(). The zlib-rs symbols are bridged from a translation unit that does not include the chromium zlib headers, since chromeconf.h renames all standard zlib identifiers (Cr_z_ mangling) - which is also why the unprefixed zlib-rs symbol names are collision-free in the binary. bench-zlib measures both backends on the streaming workload (1MB in 16KB chunks, level 6): zlib-rs measures ~34% faster deflate and ~17% faster inflate than the SIMD-enabled chromium zlib. * Use zlib-rs throughout workerd * Restore the zlib-rs rollout autogate * Fix downstream zlib header dependencies * Use zlib-rs for the zlib Bazel target Route all zlib consumers through libz-rs-sys while retaining the Chromium zlib headers. Enable carry-less multiplication on x64 to preserve CRC32 performance. * Avoid zlib module dependency on workerd Use the canonical root repository label so the zlib overlay can depend on zlib-rs without changing zlib's module metadata.github.com-cloudflare-workerd · ae4575ad · 2026-08-26
- 0.3ETVFix EventSource use-after-free EventSourceSink queued a gated callback capturing a raw EventSource pointer. If the EventSource was collected before callback execution, the callback dereferenced freed native memory. Carry a weak EventSource reference through the sink and promote it under the isolate lock before dispatch.github.com-cloudflare-workerd · 1883e0fc · 2026-08-22