Logo
Back to blog
Jul 24, 2026 · 5 min read

Engineering

Building a Rocket.rs adapter for WebAssembly

How I made Rocket compatible with Cloudflare Workers

By Vinícius Amélio

Cover for Building a Rocket.rs adapter for WebAssembly

The problem

Chances are, if you're like I was until recently and have no knowledge or experience with the Rust ecosystem, maybe because you don't know the language, you're asking yourself something like "What is this?". Well, in simple terms, Rocket is a web framework for Rust that provides a great Developer Experience: it's easy to use and has a pretty interesting feature, which is route declaration through derive macros (think of it as something like annotations in TypeScript).

Getting into this world of studying Rust, I was quickly won over by the strangely familiar way Rocket handles resource declarations in an API.

I was surprised to see that Cloudflare has Beta support for Rust in worker development, and quickly got interested in the possibility of migrating Physikos's data sync API, my weight training management app, from Deno with Fastify to Rust with Cloudflare. There were two pretty positive points to consider for adopting these technologies: I'd be able to use Cloudflare's free tier across several resources, and I'd get to put my freshly acquired Rust knowledge into practice.

But there was a problem in all of this, intrinsically tied to the architecture of Cloudflare workers: every worker is compiled to WebAssembly (WASM) with the wasm32-unknown-unknown target. In other words, at the end of the day, it's not the Rust process running your worker, but rather a single-threaded V8 isolate executing the compiled WASM.

Rocket wasn't designed for this model, and that brought some incompatibilities:

  1. It uses Hyper to accept TCP connections and Tokio's multi-thread runtime to schedule those connections. Hyper assumes socket primitives that have no equivalent in wasm32-unknown-unknown, which makes the crate fail to compile.
  2. Bound Future + Send on route handlers: Rocket requires route futures to be Send, because in the "server" world requests can get dispatched across Tokio's pool threads. However, inside a worker, calls to Cloudflare's bindings resolve through wasm_bindgen_futures, which doesn't implement the Send trait. A route handler that awaits a D1 call simply doesn't compile on stock Rocket: the compiler rejects placing a !Send future where the framework requires Send.
  3. Assumed buffering: Rocket's request/response APIs (data guards, streaming, responders) were designed on top of Hyper's I/O stack, which has its own notion of stream. Without adaptation, the natural path would be buffering the entire request/response body in memory before handing it off to the worker side. It would even work, but then I'd be giving up real streaming, resulting in high memory usage for large payloads.

How I solved these problems

Given the problems above, it's fair to say Rocket doesn't lack a WASM port. What exists are core pieces of the framework that start from assumptions the worker isolate doesn't satisfy.

With that, I built Comet. It's a patched Rocket plus an adapter, not a parallel fork.

That means I didn't reimplement Rocket: I vendor a patched copy of core/lib, core/http, and core/codegen, and apply two main adaptations:

  • I separated Rocket's "transport" surface from its "routing" surface, so the part that matters compiles to wasm32-unknown-unknown without dragging Hyper/Tokio along. This same patch also swaps route futures to local-boxed, removing the Send bound exactly where it broke. Routes in workers can await !Send futures directly, with no manual wrapper of any kind.
  • I added a variant that lets a worker request's body get streamed straight into Rocket's Data, instead of being fully buffered like I did initially. That's what guarantees a /stream route has a real time-to-first-byte, instead of waiting for the whole body to arrive before processing.

On top of this patched Rocket version, Comet exposes a thin adapter that:

  • Receives the request Cloudflare hands to the handler;
  • Converts that request into Rocket's internal format and injects it into the routing engine, without opening any socket;
  • Lets Rocket run its normal pipeline (guards, fairings, handlers, responders);
  • Converts Rocket's response into the format Cloudflare expects, streaming the body when the responder is a stream.

Care taken

It would be very easy to make these blunt changes to the project and simply expect performance bottlenecks to pile up, given I was dealing with an I/O adaptation layer. So I took some care with performance:

  • Real streaming, both ways: request and response flow through Rocket as a stream, not as a fully buffered payload.
  • No parallel runtime overhead: by not bringing Tokio/Hyper into the worker binary, Comet avoids paying for a multi-thread scheduler the isolate would never use. Handlers run directly on the worker's own event loop.
  • Typed bindings with no extra copies: D1, Queues, KV, R2, service bindings, and Hyperdrive arrive as request guards managed by Rocket, avoiding intermediate serialization layers between the worker's native binding and the route handler.
  • Performance regression validation in the pipeline: there's a performance test that runs against the local wrangler dev environment as part of the vendor drop validation, specifically to catch latency regressions introduced by changes to the vendored Rocket or the adapter.
DXBackendArchitecture

Want to talk about this topic?

Let's exchange ideas about software, product, and architecture.

Get in touch