CLI tool
cloudflare/workers-rs avatar
cloudflare/workers-rs

cloudflare/workers-rs: writing Cloudflare Workers in Rust

Write Cloudflare Workers in 100% Rust via WebAssembly

3,694 stars439 forksRustApache-2.0

At a glance

What is it?
workers-rs is Cloudflare's set of Rust bindings for the Workers runtime, compiled to wasm32-unknown-unknown and built with worker-build. It suits Rust teams who want Workers bindings without dropping to JavaScript, and it asks you to accept a build step between cargo and wrangler.
Who is it for?
Adopt workers-rs if your team already writes Rust and you want KV, Durable Objects, queues and R2 bindings behind types the compiler checks, and you can live with worker-build sitting between cargo and wrangler.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What workers-rs solves, and who it is for

Cloudflare Workers run JavaScript and WebAssembly, and the platform's own bindings are JavaScript objects. workers-rs is the crate that puts Rust types in front of those objects. The README opens with the claim it is making: "Ergonomic Rust bindings to Cloudflare Workers environment. Write your entire worker in Rust!" The operative word is bindings. This is not a Rust runtime and not a port of the Workers runtime; it is a layer of Rust APIs and proc macros that compile to wasm32-unknown-unknown and talk to the same host objects a JavaScript worker would.

The audience is narrow and specific. You need to be comfortable in Rust, comfortable with async Rust, and willing to run a build pipeline that is not just wrangler. In exchange you get compile-time checking over bindings that are otherwise stringly typed in JavaScript: KV namespaces, Durable Object stubs, secrets, variables, queues, and the request and response types. The repository layout reflects that ambition. There are separate crates for the user-facing API (worker), the proc macros (worker-macros), the raw host bindings (worker-sys), and the build tool (worker-build), plus a templates directory and around twenty examples covering KV, queues, RPC, TCP sockets, email, and tracing.

If your worker is a small fetch handler that rewrites headers, none of this pays for itself. The value appears when the worker grows into a service with typed state, several bindings, and a test suite you want to run in CI.

The event macro, the Router, and what actually runs where

A worker written with this crate starts at an entrypoint annotated with #[event(fetch)]. The macro generates the glue that the Workers runtime expects, and the function you write takes a Request, an Env, and a Context. The README's first example is a POST handler that reads a multipart form, matches on FormEntry::File, and returns the uploaded file's byte length, with a 405 for any other method. That example is worth reading closely because it shows the shape of the API: req.form_data().await returns a Result, form.get("file") returns an Option<FormEntry>, and the variants are File and Field.

Env is where bindings live. The README states that all bindings to your script, meaning Durable Object and KV namespaces, secrets, variables and version, are reachable from the env parameter passed to the entrypoint and from the ctx argument inside a route handler when you use Router. The Durable Object example in the README calls ctx.durable_object("CHATROOM"), then id_from_name("A") on the namespace, then get_stub() on the id. Three fallible steps, each returning Result, which is the pattern throughout: the crate pushes failure into the type system rather than throwing.

Router is the second layer. It supports parameters such as /account/:id and wildcards such as /file/*pathname, and handlers receive a RouteContext carrying shared data, route params, and Env bindings. A handler reads a param with ctx.param("id") and a KV namespace with ctx.kv("ACCOUNTS"), then calls .json::<Account>() on the value. Router::with_data(D) exists for sharing arbitrary state across routes. The README also shows a /upload route that distinguishes FormEntry::File from FormEntry::Field and a /echo-bytes route that rejects payloads under 1024 bytes before echoing them back with Response::from_bytes.

The http feature flag is the other architectural decision worth knowing about. Introduced in worker 0.0.21, it starts replacing the crate's custom types with types from the http crate. The README lists the consequences: a Body type that implements http_body::Body as a wrapper around web_sys::ReadableStream, a fetch handler whose request argument becomes http::Request<worker::Body>, a return type of http::Response<B> where B is any http_body::Body<Data=Bytes>, and matching changes to Fetcher::fetch_request. The payoff is that frameworks like axum can be used directly, and the README points at examples/axum for a router that returns http::Response<axum::body::Body>. Because try_from conversions exist in both directions between worker::Request and http::Request<worker::Body>, and between the response types, you can migrate incrementally instead of rewriting the worker.

Installing workers-rs and getting a first worker running

The README does not ask you to add the crate by hand first. It points at cargo generate, and there are several templates to choose from. During generation you are prompted about enabling panic=unwind and abort recovery, which the README ties to its Panic Recovery section. The generated project has a src/lib.rs, and the README says to start there.

bash
cargo generate cloudflare/workers-rs

After generation you should see the project layout with src/lib.rs. Any local or remote crate works as long as it compiles to wasm32-unknown-unknown, which is the constraint that decides most dependency questions early.

Running locally goes through wrangler, which the README names as the tool for running and publishing your Worker. The command is not wrapped by anything in the crate.

bash
npx wrangler dev

Going live is the same wrangler command you would use for a JavaScript worker, with routes and zones configured in the worker's wrangler.toml file.

bash
# configure your routes, zones & more in your worker's `wrangler.toml` file
npx wrangler deploy

If you want wrangler installed on the machine rather than invoked through npx, the README points at the wrangler repository for instructions. For the first real use, the smallest useful thing is the fetch entrypoint from the README: a handler that rejects non-POST methods with Response::error("Method Not Allowed", 405) and otherwise inspects req.form_data().await. That gives you one binding-free worker you can deploy and one place to add an Env binding next.

Where workers-rs stops being the right tool

The first limitation is the build chain. worker-build is a crate in this workspace, and the repository's own test scripts invoke it directly, for example ../target/debug/worker-build --dev before running vitest. That means the path from Rust source to a deployable worker has a step wrangler does not perform for you. The README's quickstart hides this behind cargo generate, but a project that already has a build system has to accommodate it.

The second is the http feature flag's cost. Making the fetch handler speak http::Request<worker::Body> is what lets axum run inside a worker, and it is also a type-level commitment across your handler signatures and any Fetcher::fetch_request calls. The try_from conversions soften the migration, but a mixed codebase carries both vocabularies at once.

The third is that several operational questions are simply not answered in the README. It does not document rollback, it does not give a wasm binary size ceiling, and it does not describe cold start behaviour. Those are not oversights to paper over with a guess; they are things to measure on your own worker before you commit. There is a benchmark directory in the repository, which tells you the maintainers care about performance, but the README does not publish numbers, and this article will not invent any.

Finally, consider the case where the answer is JavaScript. If your worker is a few lines of routing and a fetch call, the Rust toolchain, the wasm target, and worker-build are pure overhead. The bindings earn their keep when the worker has real structure.

workers-rs against the JavaScript and TypeScript path

The honest alternative is not another Rust framework. It is writing the worker in JavaScript or TypeScript against @cloudflare/workers-types, which the repository itself depends on for its TypeScript side. The difference in approach is where correctness is enforced. In TypeScript you get types at compile time and they disappear at runtime; a KV binding is a property on env that you assert exists. In workers-rs the binding lookup is a call that returns a Result, so ctx.kv("ACCOUNTS") can fail and the compiler makes you handle it, and ctx.param("id") returns an Option you must unwrap deliberately.

That difference cuts both ways. Rust's version is more verbose at every call site, and the README's own examples show the ceremony: nested matches, explicit error responses for each failure branch, and Response::error calls to produce the 400 and 405 cases. What you buy is that a renamed binding or a changed route param surfaces during cargo build rather than in production. What you pay is a longer edit-compile-deploy loop and a toolchain your JavaScript colleagues may not have installed.

There is a middle path the README supports directly: the http feature flag plus axum. If your team already has axum services, the router, extractors, and middleware patterns transfer, and the worker becomes a deployment target rather than a new framework to learn. That is a more accurate comparison than Rust versus TypeScript in the abstract.

Versioning, licensing, and what upgrading costs

The crate is published on crates.io as worker, and releases are tagged in the repository: v0.8.6 on 2026-09-15, preceded by v0.8.5 and v0.8.4 on 2026-06-12. The last push to the default branch was on 2026-09-23. The project is not archived. The version numbers matter more than usual here because the README ties a behaviour change to a specific release: the http feature flag arrived in worker 0.0.21. Pre-1.0 versioning means the API can move, and the presence of a .changeset directory in the repository root indicates changesets are used to track changes across the workspace, which is where you would look before bumping.

Upgrade cost is workspace-shaped. The Cargo.toml pins wasm-bindgen, wasm-bindgen-futures, js-sys, web-sys and wasm-streams at the workspace level, so a bump to the wasm-bindgen family is a coordinated change across worker, worker-sys, and worker-macros. The repository also carries a rust-toolchain.toml, which means the project states the toolchain it builds against; matching that locally avoids a class of confusing build failures. If you vendor worker-build or wasm-bindgen, note that the repository's build script compiles a local wasm-bindgen-cli binary from the wasm-bindgen submodule rather than taking it from crates.io.

On licensing, the crate is Apache-2.0. The repository's package.json declares "MIT OR Apache-2.0" for the monorepo tooling, so the two are not identical, and a legal review should look at the crate metadata rather than this article. Apache-2.0 includes an explicit patent grant and requires you to preserve notices and state changes; that is a description of the licence text, not advice on your situation.

Editorial conclusion

Adopt workers-rs if your team already writes Rust and you want KV, Durable Objects, queues and R2 bindings behind types the compiler checks, and you can live with worker-build sitting between cargo and wrangler. Do not adopt it if you want a single wrangler deploy command with no Rust toolchain, or if you need a runtime guarantee the documentation does not give: the README is silent on rollback and on the wasm size ceiling, so verify both against your own worker before you commit to it.

Frequently asked questions

Who uses Cloudflare Workers?

The README gives no figures and does not describe the user base. What it does show is the kind of workload workers-rs targets: fetch handlers, KV-backed JSON APIs, Durable Objects, queues, RPC, TCP sockets and email handlers, each with an example in the repository.

What is the main difference between Web Workers and WebAssembly?

These are different things, and workers-rs only touches one of them. WebAssembly is the compilation target: the README requires that your crates compile to wasm32-unknown-unknown. A Cloudflare Worker is the deployment unit that runs the resulting module and exposes the host objects the crate binds to.

Can I use Cloudflare Workers for free?

The README says nothing about pricing or plans, so this cannot be answered from the project's documentation. What it does cover is the local path: npx wrangler dev runs your worker on your machine before you deploy with npx wrangler deploy.

What exactly are Cloudflare Workers?

The README treats Workers as the runtime your compiled wasm module is published to, with wrangler as the tool for running and publishing it. It also names the host features a worker can bind to: Durable Object and KV namespaces, secrets, variables and version, all reachable through the env parameter.

Official sources

  1. cloudflare/workers-rs on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cloudflare-workers-rs.svg)](https://hysenlabs.com/projects/cloudflare-workers-rs)