Rust on Cloudflare Workers: WebAssembly at the Edge (2026)

Max WellsMax WellsFounder of Rustify

TL;DR: Cloudflare Workers runs WebAssembly, and Rust compiles to WASM, making Rust a first-class language for edge computing. The worker crate provides ergonomic access to Workers APIs: routing, KV storage, D1 SQLite, Durable Objects, and environment bindings. Cold starts under 5ms; no servers to manage; 300+ global edge locations.

  • Deploy Rust to the edge: compile to WASM (wasm32-unknown-unknown), deploy with wrangler
  • worker crate: idiomatic Rust API for Request/Response, routing, KV, D1, Queues
  • Cold starts: WASM cold starts are 0–5ms vs 100–500ms for Node.js containers
  • CPU limits: Workers have a 10ms CPU time limit on the free tier (50ms on paid)
  • wrangler: Cloudflare's CLI: wrangler dev for local development, wrangler deploy to push

Who Should Read This?

This guide is written for backend Rust developers and systems engineers who want to deploy compute at the edge without managing servers. If you are a mid-level Rust engineer ($130K–$175K range in the US) working on APIs, data processing, or authentication middleware, this guide shows how to extend your existing Rust skills into the Cloudflare Workers environment. It is also relevant to platform engineers evaluating whether Rust-on-WASM is a viable alternative to Node.js Workers for CPU-intensive workloads (parsing, cryptography, image processing) where raw throughput matters. No prior WASM or JavaScript knowledge is required, though familiarity with async Rust is assumed.


Why Use Rust on Cloudflare Workers?

Cloudflare Workers runs on V8 isolates, not containers. WebAssembly runs natively in V8 with near-zero overhead. Rust compiles to WASM with excellent performance and a tiny binary footprint.

Advantages over Node.js Workers:

  • No runtime startup: WASM modules instantiate in microseconds vs Node.js module loading
  • Smaller binary: a Rust WASM binary for a simple API is typically 100–500 KB vs 1–5 MB for bundled JS
  • Memory safety: Rust's guarantees carry into the WASM sandbox
  • Performance: CPU-intensive tasks (parsing, crypto, compression) run significantly faster in WASM

Trade-offs:

  • More complex build pipeline (Rust → WASM → wrangler)
  • Limited WASM-compatible crates: no Tokio (no OS threads in WASM), no blocking I/O
  • The worker crate API is async but uses wasm-bindgen-futures under the hood

Bottom line: For CPU-heavy Workers (parsing, crypto, JSON processing), Rust WASM is 2–10x faster than JavaScript with 0–5ms cold starts. For I/O-heavy Workers making external API calls, the performance gap narrows. Benchmark before committing to a Rust rewrite.

Senior Rust engineers in the US working on edge infrastructure earn $185K–$230K at companies like Cloudflare, Fastly, and AWS. Understanding the WASM deployment model is increasingly a core skill for that tier of compensation.


How Do You Set Up a Rust Cloudflare Worker?

The fastest path is the official Cloudflare template: it scaffolds a complete Rust Worker project with the correct Cargo.toml, wrangler.toml, and build pipeline in under two minutes.

# Install wrangler
npm install -g wrangler
 
# Create a new Rust Worker project
npm create cloudflare@latest my-worker -- --template=cloudflare/workers-sdk/templates/worker-rust
 
# Or manually:
cargo install worker-build
# Cargo.toml
[dependencies]
worker = "0.4"
 
[lib]
crate-type = ["cdylib"]  # Required for WASM
 
# wrangler.toml
name = "my-rust-worker"
main = "build/worker/shim.mjs"
compatibility_date = "2026-01-01"
 
[build]
command = "worker-build --release"

The cdylib crate type is critical: it tells the Rust compiler to produce a C-compatible dynamic library, which is the format WASM requires. Without it, the linker produces an executable binary that cannot be loaded by the V8 WASM runtime.


How Do You Write a Basic Rust Worker?

Every Rust Worker has an event handler function: the entry point for HTTP requests.

use worker::*;
 
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    console_log!(
        "{} {}", req.method().to_string(), req.path()
    );
 
    // Route based on path
    let router = Router::new();
 
    router
        .get("/", |_req, _ctx| {
            Response::ok("Hello from Rust at the edge!")
        })
        .get_async("/user/:id", get_user)
        .post_async("/api/echo", echo_body)
        .run(req, env)
        .await
}
 
async fn get_user(req: Request, ctx: RouteContext<()>) -> Result<Response> {
    if let Some(id) = ctx.param("id") {
        let json = serde_json::json!({
            "id": id,
            "name": format!("User {}", id)
        });
        Response::from_json(&json)
    } else {
        Response::error("Missing id", 400)
    }
}
 
async fn echo_body(mut req: Request, _ctx: RouteContext<()>) -> Result<Response> {
    let body = req.text().await?;
    Response::ok(format!("Echo: {}", body))
}

The #[event(fetch)] macro generates the WASM export that Cloudflare's runtime calls for each HTTP request. The Router type provides a builder-style API for matching paths and HTTP methods, familiar to anyone who has used Axum or Actix.


3 spots open this month → Check if you are eligible.

We help experienced developers transition into Rust roles at €80K–€150K+ in Europe or $130K–$200K+ in the US.

How Does KV Storage Work in Rust Workers?

Cloudflare KV is a key-value store distributed across all edge locations: eventual consistency, read-heavy workloads.

# wrangler.toml: bind KV namespace
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"
use worker::*;
 
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    let kv = env.kv("CACHE")?;
 
    match req.path().as_str() {
        "/set" => {
            // Store value with 1-hour TTL
            kv.put("greeting", "Hello, World!")?
                .expiration_ttl(3600)
                .execute()
                .await?;
            Response::ok("Stored")
        }
        "/get" => {
            match kv.get("greeting").text().await? {
                Some(value) => Response::ok(value),
                None => Response::error("Not found", 404),
            }
        }
        "/delete" => {
            kv.delete("greeting").await?;
            Response::ok("Deleted")
        }
        _ => Response::error("Not found", 404),
    }
}

KV is eventually consistent. Writes propagate to all edge locations within seconds, but reads at a given edge location may briefly return stale data after a write. For session tokens, feature flags, or static configuration that does not change frequently, KV is ideal. For data requiring strong consistency (account balances, inventory), use D1 with transactions or Durable Objects.

Bottom line: Use KV for caching and configuration (eventual consistency is fine), D1 for relational data requiring SQL and transactions, and Durable Objects only when you need linearizable state like WebSocket sessions or rate limiters. Mixing them incorrectly is the most common Cloudflare Workers architecture mistake.


How Do You Use D1 SQLite in a Rust Worker?

D1 is Cloudflare's serverless SQLite: runs at the edge, replicated globally, accessible from Workers.

# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-d1-database-id"
use worker::*;
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    email: String,
}
 
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    let db = env.d1("DB")?;
 
    match (req.method(), req.path().as_str()) {
        (Method::Get, "/users") => {
            let result = db
                .prepare("SELECT id, name, email FROM users LIMIT 20")
                .all()
                .await?;
 
            let users: Vec<User> = result.results()?;
            Response::from_json(&users)
        }
        (Method::Post, "/users") => {
            let mut req = req;
            let user: serde_json::Value = req.json().await?;
 
            db.prepare("INSERT INTO users (name, email) VALUES (?1, ?2)")
                .bind(&[
                    user["name"].as_str().unwrap_or("").into(),
                    user["email"].as_str().unwrap_or("").into(),
                ])?
                .run()
                .await?;
 
            Response::ok("Created")
        }
        _ => Response::error("Not found", 404),
    }
}

D1 uses SQLite under the hood, which means full SQL support including transactions, indexes, foreign keys, and joins. The results::<T>() method deserializes rows directly into typed Rust structs via serde, the same pattern as SQLx's fetch_all::<T>(). D1 read replicas are automatically placed at the nearest edge location; writes are routed to the primary with global replication following.


How Do You Manage Environment Variables and Secrets?

Secrets in Cloudflare Workers are encrypted at rest and never appear in your source code or wrangler.toml: they are injected at runtime.

# wrangler.toml: plaintext vars
[vars]
API_VERSION = "v2"
MAX_ITEMS = "100"
# Secrets (encrypted, not in wrangler.toml)
wrangler secret put API_KEY
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    // Read env var
    let version = env.var("API_VERSION")?.to_string();
 
    // Read secret
    let api_key = env.secret("API_KEY")?.to_string();
 
    // Validate API key from request header
    let auth = req.headers().get("Authorization")?.unwrap_or_default();
    if auth != format!("Bearer {}", api_key) {
        return Response::error("Unauthorized", 401);
    }
 
    Response::ok(format!("API {}: authorized", version))
}

The distinction between env.var() and env.secret() is important: vars are plaintext in your repository's wrangler.toml and appropriate for non-sensitive configuration. Secrets are stored encrypted in Cloudflare's vault and require wrangler secret put to set or rotate, and they never appear in version control.


How Do You Develop Locally and Deploy to the Edge?

wrangler dev runs a local Cloudflare emulator (Miniflare) that faithfully simulates Workers, KV, D1, and Durable Objects: you get production-like behavior without deploying.

# Local development: runs miniflare (local Cloudflare emulator)
wrangler dev
 
# Deploy to Cloudflare edge
wrangler deploy
 
# View live logs
wrangler tail
 
# Run D1 migrations
wrangler d1 migrations apply my-db
wrangler d1 execute my-db --file=./schema.sql

Build output: worker-build compiles Rust to WASM and generates a JS shim.

build/
└── worker/
    ├── index.wasm    ← compiled Rust code (~200-500 KB for typical API)
    └── shim.mjs      ← JS glue code (Cloudflare requires a JS entry point)

The wrangler tail command is particularly useful in production: it streams structured logs from all edge locations in real time, including your console_log!() calls from Rust. This replaces the need for a separate logging infrastructure during early stages.


What Common Mistakes Do Rust Developers Make When Building Cloudflare Workers?

  • Using Tokio or OS threads in WASM: The wasm32-unknown-unknown target has no OS thread support. Any crate that pulls in Tokio's multi-threaded runtime, std::thread::spawn, or blocking I/O will fail to compile. Audit your dependency tree with cargo tree before attempting a WASM build. Use wasm-bindgen-futures for async, which the worker crate handles automatically.

  • Forgetting crate-type = ["cdylib"]: Without this, Rust produces a static executable binary that cannot be loaded as a WASM module. The error messages from wasm-pack or worker-build are not always clear about the root cause. Always include cdylib in the [lib] section of Cargo.toml.

  • Blocking in the CPU limit window: Cloudflare free tier allows only 10ms CPU time per request. Code that does heavy computation synchronously (parsing large files, running regex on big inputs) will hit this limit and return an error. Chunk large tasks or upgrade to the paid tier (50ms CPU). Profile with wrangler dev before deploying.

  • Storing secrets in wrangler.toml: wrangler.toml is committed to version control. Any value placed under [vars] becomes plaintext in your repository. Use wrangler secret put for all credentials, tokens, and keys. The env.secret() API makes it equally simple to read them at runtime.

  • Ignoring KV consistency semantics: KV is eventually consistent: a value written in one region may not be immediately visible in another. Developers used to databases with read-after-write consistency are surprised when a value they just wrote is missing on the next read. Design around this: use KV for static configuration and caching, not for data requiring immediate global consistency.

  • Using the wrong target triple: Cloudflare Workers requires wasm32-unknown-unknown, not wasm32-wasi (which assumes a WASI runtime) or wasm32-wasip1. The worker-build tool sets this automatically, but if you are integrating with a custom build pipeline, specify --target wasm32-unknown-unknown explicitly.


Is Rust on Cloudflare Workers Worth Using for a Structured Learning Path?

If you want to go from knowing Rust basics to shipping production edge compute, a structured path accelerates the journey dramatically. Rustify's 9-week bootcamp covers async Rust, WASM compilation targets, and deploying services to real infrastructure, with 1:1 coaching from engineers who have shipped Rust in production. Edge computing with Cloudflare Workers is part of the curriculum because it represents one of the clearest paths to deploying Rust without managing servers.


Frequently Asked Questions

No. WASM in Workers is single-threaded and has no OS thread support. Tokio's multi-threaded runtime won't compile for wasm32-unknown-unknown. Use wasm-bindgen-futures for async, which the worker crate does internally. Most async I/O just works with the provided async primitives; CPU-blocking tasks need to be chunked or offloaded. If you find yourself needing thread pools or OS-level concurrency, Cloudflare Workers is not the right deployment target. Consider a traditional server with Axum or Actix instead.

For CPU-intensive work (parsing, crypto, JSON processing), Rust WASM is typically 2–10x faster. For I/O-bound work (fetching external APIs, reading KV), performance is similar since both await the same network. The main benefit for I/O-bound Workers is the smaller binary and faster cold start. Benchmark with wrangler dev before committing to a Rust rewrite. If your Worker spends 90% of its time on network I/O, the performance gain from Rust will be marginal.

Cloudflare limits Workers to 10 MB compressed. A typical Rust WASM binary is 200 KB–2 MB, well within limits. Use wasm-opt and wasm-pack's --release flag to optimize binary size. You can also run wasm-opt -O3 (from the Binaryen toolchain) on the output .wasm file to squeeze an additional 20–40% size reduction, which helps with download time to edge locations where the module is not yet cached.

Cloudflare Workers free tier includes 100,000 requests/day and up to 10ms CPU time per request. The paid tier ($5/month) includes 10 million requests/month and 50ms CPU time. For most projects, the free tier is sufficient during development and early production. D1 free tier includes 5 GB storage and 25 million row reads per day, more than enough for prototypes and small-scale applications.

Durable Objects provide strongly consistent, single-threaded execution with persistent storage: ideal for stateful workloads like WebSocket connections, game sessions, or rate limiters. Each Durable Object is a single instance with a unique ID; requests to the same ID are routed to the same instance. KV, by contrast, is eventually consistent and optimized for high read throughput across many readers. Use Durable Objects when you need linearizability; use KV when you need low-latency global reads of infrequently changing data.

Not directly. Workers cannot open long-lived TCP connections. You can connect to Postgres via Hyperdrive (Cloudflare's connection pooling proxy that speaks HTTP), or use Neon's serverless HTTP driver, or PlanetScale's HTTP API. The Rust worker crate can make HTTP fetch calls to any of these. D1 is the zero-configuration option that eliminates this complexity for most use cases.

console_log!() from the worker crate writes structured logs visible in wrangler tail and in the Cloudflare dashboard. For production observability, Workers Analytics Engine lets you write structured time-series data to Cloudflare's analytics platform. For external logging, Workers can send logs via HTTP fetch to Datadog, Grafana Cloud, or any HTTP-ingesting logging service, though this adds latency to the log path.


Keep Reading

Sources

Ready to Land a $80-120k Rust Job?