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

Engineering

Criando um adapter de Rocket.rs para WebAssembly

Como tornei Rocket compatível com Workers da Cloudflare

By Vinícius Amélio

Cover for Criando um adapter de Rocket.rs para WebAssembly

O problema

Muito possivelmente, se você, assim como eu até pouco tempo atrás, não tem conhecimento ou experiência sobre o ecossistema Rust, talvez por não conhecer a linguagem, deve estar se questionando algo como "O que é isso?". Bom, em termos simples, Rocket é um framework web para Rust que fornece uma ótima Developer Experience: é fácil de usar e tem um ponto bem interessante, que é a declaração de rotas através do uso de derive macros (pense em algo como anotações no TypeScript).

Ao ingressar nesse mundo de estudos sobre Rust, fiquei rapidamente encantado pela forma, estranhamente familiar, como o Rocket lida com as declarações de recursos numa API.

Fiquei surpreso ao ver que a Cloudflare está com suporte, em Beta, para Rust no desenvolvimento de seus workers, e rapidamente me interessei pela possibilidade de migrar a API de sincronização de dados do Physikos, meu app de gestão de treinos de musculação, de Deno com Fastify para Rust com Cloudflare. Eram dois pontos bastante positivos para considerar a adoção dessas tecnologias: eu conseguiria utilizar o tier free da Cloudflare em vários recursos, e poderia pôr em prática meus conhecimentos recém adquiridos em Rust.

Mas havia um problema nisso tudo, ligado intrinsecamente à arquitetura dos workers da Cloudflare: cada worker é compilado para WebAssembly (WASM) com target wasm32-unknown-unknown. Ou seja, no final do dia, não é o processo Rust que está executando o seu worker, mas sim uma isolate V8 single-threaded que executa o WASM compilado.

O Rocket não foi desenhado para esse modelo, e isso trouxe algumas incompatibilidades:

  1. Ele utiliza o Hyper para aceitar conexões TCP e o runtime multi-thread do Tokio para fazer scheduling dessas conexões. Hyper assume primitivas de socket que não possuem equivalentes em wasm32-unknown-unknown, isso faz com que o crate não compile.
  2. Bound Future + Send nos handlers de rota: o Rocket exige que os futures de rota sejam Send, porque no mundo "servidor" as requisições podem ser despachadas entre threads de pool do Tokio. Porém, dentro de um worker, as chamadas às bindings da Cloudflare resolvem através de wasm_bindgen_futures, que não implementa o trait Send. Um handler de rota que faz await numa chamada de D1 dentro do Rocket publicado simplesmente não compila: o compilador rejeita colocar um future !Send onde o framework exige Send.
  3. Buffering assumido: as APIs de request/response do Rocket (data guards, streaming, responders) foram desenhadas em cima da pilha de I/O do Hyper, que tem sua própria noção de stream. Sem adaptação, o caminho natural seria bufferizar o corpo inteiro de request/response em memória antes de repassar pro lado do worker. Até iria funcionar, mas aí eu estaria abrindo mão de streaming de verdade, resultando em uso de memória alto para payloads grandes.

Como eu resolvi esses problemas?

Diante dos problemas apresentados, é justo dizer que não existe uma falta de porte para WASM no Rocket. Existem, sim, peças centrais do framework que partem de premissas que a isolate do worker não satisfaz.

Com isso, criei o Comet. Ele é um Rocket com patch + adapter, e não um fork paralelo.

Isso significa que não reimplementei o Rocket: eu vendorizo uma cópia patcheada de core/lib, core/http e core/codegen e aplico duas adaptações principais:

  • Passei a separar a superfície de "transporte" da superfície de "roteamento" do Rocket, de forma que a parte que interessa compila para wasm32-unknown-unknown sem trazer Hyper/Tokio junto. Esse mesmo patch também troca os futures de rota para local-boxed, removendo o bound Send exatamente onde ele quebrava. Rotas em workers podem dar await direto em futures !Send sem nenhum tipo de wrapper manual.
  • Adicionei uma variante permitindo que o corpo de uma request de worker seja streamado direto pra dentro do Data do Rocket, em vez de bufferizado inteiro, como fiz inicialmente. Isso é o que garante que uma rota /stream tenha time-to-first-byte real, e não espere o corpo todo chegar para só então processar.

Por cima dessa versão patcheada do Rocket, o Comet expõe um adapter fino que:

  • Recebe a request que a Cloudflare entrega no handler;
  • Converte essa request pro formato interno do Rocket e injeta na engine de rotas, sem abrir socket nenhum;
  • Deixa o Rocket rodar sua pipeline normal (guards, fairings, handlers, responders);
  • Converte a resposta do Rocket para o formato esperado pela Cloudflare, streamando o corpo quando o responder é uma stream.

Cuidados

Seria muito fácil fazer essas alterações brutas no projeto e simplesmente esperar que gargalos de performance acontecessem freneticamente, por estar lidando com uma camada de adaptação de I/O. Por isso, tomei alguns cuidados quanto à performance:

  • Streaming de verdade, nos dois sentidos: Request e response passam pelo Rocket como stream, não como buffer completo.
  • Sem overhead de runtime paralelo: Por não trazer Tokio/Hyper pro binário do worker, o Comet evita pagar o custo de um scheduler multi-thread que a isolate nunca vai usar. Os handlers rodam diretamente sobre o event loop do próprio worker.
  • Bindings tipadas e sem cópias extras: D1, Queues, KV, R2, service bindings e Hyperdrive chegam como request guards gerenciados pelo Rocket, evitando camadas de serialização intermediárias entre o binding nativo do worker e o handler da rota.
  • Validação de regressão de performance na pipeline: existe um teste de performance que roda contra o ambiente wrangler em dev local como parte da validação do vendor drop, especificamente para pegar regressão de latência introduzida por mudanças no Rocket vendorizado ou no adapter.
DXBackendArchitecture

Want to talk about this topic?

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

Get in touch