Cloudflare Workers déploie du code dans plus de 330 villes en moins de 30 secondes après le push. Pour des charges CPU sensibles — parsing, crypto, rewriting d’images — exécuter du Rust compilé en WebAssembly y est plus rapide qu’un Worker JavaScript et démarre en quelques dizaines de microsecondes. La crate worker (workers-rs 0.8.3) encapsule l’API Workers en Rust idiomatique et la chaîne worker-build + wrangler automatise la compilation, le bundling et le déploiement.
Ce tutoriel pas-à-pas part d’un poste vide pour déployer un service Rust complet sur l’edge : routes, KV, D1, R2, observabilité. Pour le panorama serverless WebAssembly et la comparaison avec Wasmtime ou Spin, on se référera au guide principal sur WebAssembly en production. Les commandes ont été exécutées avec Rust 1.83, wrangler 4.x, worker-build 0.7.4 et la crate worker 0.8.3.
Étape 1 — Installer les outils requis
Trois outils sont nécessaires : Rust avec la cible wasm32-unknown-unknown, wrangler (CLI Cloudflare en Node) et cargo-generate pour scaffolder le projet. worker-build sera ajouté comme dépendance du projet et n’a pas besoin d’être installé globalement.
rustup target add wasm32-unknown-unknown
cargo install cargo-generate
npm create cloudflare@latest -- --type=hello-worker --framework=rust
L’alternative manuelle, si l’on veut le scaffold minimal officiel sans passer par create-cloudflare, utilise cargo-generate. C’est la voie historique, encore documentée par Cloudflare.
cargo generate cloudflare/workers-rs
Le template prompt le nom du projet et active panic=unwind par défaut. Depuis workers-rs 0.6, le runtime Workers utilise WebAssembly Exception Handling pour récupérer d’un panic Rust sans tuer l’isolat : c’est ce qu’on appelle le panic recovery, et il faut le laisser activé en production.
Étape 2 — Anatomie du projet généré
Le générateur crée trois fichiers clés à la racine. Cargo.toml liste les dépendances Rust et déclare le type de bibliothèque. wrangler.toml décrit l’environnement Cloudflare (nom du Worker, compatibilité, bindings). src/lib.rs contient le handler.
Le Cargo.toml ressemble à ceci, avec workers-rs en version récente.
[package]
name = "edge-demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
worker = { version = "0.8", features = ["http", "d1"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
console_error_panic_hook = "0.1"
[profile.release]
opt-level = "s"
lto = true
Le wrangler.toml définit la compatibilité Workers, qui détermine quelles API JavaScript et WebAssembly sont disponibles. La date de compatibilité fige le comportement même si Cloudflare change l’API par la suite — c’est essentiel pour des déploiements reproductibles.
name = "edge-demo"
main = "build/worker/shim.mjs"
compatibility_date = "2026-05-01"
compatibility_flags = ["nodejs_compat_v2"]
[build]
command = "cargo install --quiet worker-build && worker-build --release"
La commande worker-build --release est l’orchestrateur clé : elle invoque cargo build --target wasm32-unknown-unknown --release, exécute wasm-bindgen et génère le shim JavaScript qui présente le module au runtime Workers.
Étape 3 — Écrire le premier handler avec routing
On remplace le contenu de src/lib.rs par un router minimaliste. La macro #[event(fetch)] marque le point d’entrée appelé pour chaque requête HTTP. On utilise Router pour mapper les chemins.
use worker::*;
#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
console_error_panic_hook::set_once();
let router = Router::new();
router
.get("/", |_, _| Response::ok("Hello edge!"))
.get_async("/version", |_, _| async move {
Response::from_json(&serde_json::json!({
"runtime": "cloudflare-workers",
"engine": "v8-wasm",
"build_at": env!("CARGO_PKG_VERSION"),
}))
})
.run(req, env)
.await
}
console_error_panic_hook::set_once() redirige les panics Rust vers console.error, ce qui les rend lisibles dans les logs Wrangler. env!("CARGO_PKG_VERSION") est résolu à la compilation : la version du Cargo.toml est embarquée dans le binaire.
Étape 4 — Tester en local avec wrangler dev
Avant de déployer, on valide le comportement avec wrangler dev. Cette commande lance un proxy local qui charge le Worker dans une instance du moteur Workers (workerd), c’est-à-dire le même runtime que la production. Pas d’émulation approximative.
npx wrangler dev
La sortie affiche un endpoint http://localhost:8787. On teste les deux routes du router avec curl. La première retourne « Hello edge! » ; la seconde, un objet JSON.
curl -s http://localhost:8787/
curl -s http://localhost:8787/version | jq
Si le binaire ne compile pas, wrangler dev rapporte l’erreur Rust dans le terminal. Le rebuild est automatique : à chaque sauvegarde de fichier, worker-build est invoqué et l’isolat est rechargé. Pour un projet de taille moyenne, le temps de cycle s’établit autour de 2-4 secondes.
Étape 5 — Déployer en production
Le déploiement se fait avec wrangler deploy. La première invocation prompt pour s’authentifier sur le compte Cloudflare via OAuth dans le navigateur.
npx wrangler login
npx wrangler deploy
La sortie attendue annonce « Total Upload » (la taille du module compressé), « Worker Startup Time » et l’URL finale du type edge-demo.<votre-sous-domaine>.workers.dev. La startup time est l’indicateur clé : pour un Worker Rust correctement optimisé, on vise sous 20 ms. Au-dessus, Cloudflare refuse parfois la mise en ligne ou applique des limites strictes — on optimise le binaire (opt-level = "s", dépendances minimales).
Étape 6 — Brancher KV pour la persistance clé/valeur
Workers KV est un store distribué lazily consistent, parfait pour cache et configuration. On le crée d’abord côté Cloudflare, puis on le binde au Worker.
npx wrangler kv namespace create CACHE
# La commande retourne un id, à coller dans wrangler.toml
On ajoute le binding dans wrangler.toml. Le nom CACHE devient une propriété de env dans le handler Rust.
[[kv_namespaces]]
binding = "CACHE"
id = "<id retourné par la commande>"
Côté Rust, on utilise env.kv("CACHE") pour récupérer un client typé. La crate worker fournit les méthodes get, put, delete, list avec gestion des TTL.
router.get_async("/cache/:key", |_, ctx| async move {
let key = ctx.param("key").unwrap_or(&"".to_string()).clone();
let kv = ctx.kv("CACHE")?;
match kv.get(&key).text().await? {
Some(v) => Response::ok(v),
None => Response::error("not found", 404),
}
})
Les lectures KV à l’edge tournent autour d’une médiane de 12 millisecondes, avec des latences sub-milliseconde sur les clés fréquemment lues (cache local de l’edge). Les écritures sont éventuellement cohérentes, avec une borne supérieure documentée de 60 secondes pour la propagation globale. C’est l’outil idéal pour des feature flags, des sessions courtes ou un cache de réponses.
Étape 7 — Brancher D1 pour le relationnel
D1 est la base SQLite serverless de Cloudflare. Elle s’adresse aux charges relationnelles légères à modérées, avec une réplication régionale automatique. On la crée via wrangler.
npx wrangler d1 create edge-demo-db
La commande affiche un database_id à coller dans wrangler.toml. On définit le schéma dans un fichier schema.sql et on l’applique localement puis en production.
[[d1_databases]]
binding = "DB"
database_name = "edge-demo-db"
database_id = "<id retourné par la commande>"
Pour exécuter le schéma, on utilise wrangler d1 execute avec un fichier SQL. L’option --remote applique en production, son absence applique sur la base locale émulée.
echo "CREATE TABLE orders (id TEXT PRIMARY KEY, amount REAL, currency TEXT);" > schema.sql
npx wrangler d1 execute edge-demo-db --file=schema.sql --remote
Côté Rust, on accède à la base via env.d1("DB"). Les requêtes sont préparées avec prepare puis exécutées via first, all ou run.
router.get_async("/orders", |_, ctx| async move {
let db = ctx.env.d1("DB")?;
let res = db.prepare("SELECT id, amount, currency FROM orders LIMIT 50")
.all().await?;
Response::from_json(&res.results::<serde_json::Value>()?)
})
Étape 8 — Stocker des fichiers avec R2
R2 est le stockage objets compatible S3 de Cloudflare, sans frais de sortie. On l’utilise pour images, exports CSV, archives. La création se fait via wrangler, le binding dans le TOML est identique aux autres services.
npx wrangler r2 bucket create edge-demo-assets
On déclare le binding et l’on accède au bucket via env.bucket("ASSETS"). Une route minimale qui sert un objet stocké :
router.get_async("/asset/:key", |_, ctx| async move {
let key = ctx.param("key").unwrap_or(&"".to_string()).clone();
let bucket = ctx.env.bucket("ASSETS")?;
match bucket.get(&key).execute().await? {
Some(obj) => {
let body = obj.body().ok_or(Error::from("empty body"))?;
Response::from_stream(body.stream()?)
}
None => Response::error("not found", 404),
}
})
Étape 9 — Observer le Worker en production
Trois outils permettent de comprendre ce qui se passe une fois le Worker en ligne. wrangler tail ouvre un flux des logs console.* en temps réel. C’est l’équivalent de journalctl -f pour Workers.
npx wrangler tail edge-demo --format pretty
Le dashboard Cloudflare affiche des métriques agrégées : requêtes par seconde, erreurs, durée CPU. Pour une observabilité plus fine — traces OpenTelemetry, métriques custom — on configure un Trace Worker qui pousse vers un endpoint OTLP. Cloudflare documente le pattern dans la section Observability de la doc Workers.
Quatrième levier : la health check via les Smart Placement et les Durable Objects pour des scénarios stateful. Ce sont des extensions de cet article que l’on traite dans les guides dédiés.
Étape 10 — Erreurs fréquentes
| Symptôme | Cause | Résolution |
|---|---|---|
Error: Worker exceeded startup time limit |
Binaire trop gros ou trop d’init | Réduire dépendances, profil release optimisé taille |
Error: KV binding 'CACHE' not found |
wrangler.toml non aligné au code |
Vérifier que binding correspond exactement |
error[E0432]: unresolved import 'worker' |
worker non listé dans Cargo.toml |
Ajouter la dépendance et recompiler |
npx wrangler: command not found |
Node non installé ou PATH cassé | Installer Node 20+ et relancer |
| Erreur 1101 en production | Panic Rust non récupéré | Vérifier que panic=unwind est actif |
| Coût de requête plus élevé qu’attendu | Sub-requests KV/D1 non comptabilisées initialement | Lire la grille Workers Paid : 5 USD/mois forfait + 10 M requêtes incluses puis 0,50 USD/M req au-delà |
Optimiser le démarrage à froid et la mémoire
Cloudflare exécute chaque Worker dans un isolat V8 court ; la durée d’initialisation pèse directement sur les latences perçues. Trois leviers ont un impact mesurable sur un Worker Rust + WebAssembly.
D’abord, la taille du binaire. Le ratio bytes téléchargés/parsés se traduit directement en startup time. Le profil release avec opt-level = "s" et lto = true divise typiquement la taille par deux par rapport à un release par défaut. On évite aussi les crates qui activent du networking lourd ou des futures complexes — sur Workers, beaucoup d’API standard sont déjà couvertes par worker et dupliquer la pile ajoute du poids inutile.
Ensuite, la déshydratation des dépendances lourdes. Si l’on a besoin de regex compilées, on les déclare en static avec once_cell::sync::Lazy pour les compiler à l’initialisation de l’isolat plutôt qu’à chaque requête. Idem pour les clients HTTP, les configurations parsées, les schémas JSON. La sortie de wrangler deploy affiche la Worker Startup Time ; l’objectif est de la maintenir sous 50 ms.
Enfin, le partage d’état entre requêtes. Workers met en cache les modules WebAssembly compilés et réutilise les isolats. Mais un isolat peut servir des milliers de requêtes avant d’être recyclé : éviter les fuites mémoire (Vec qui grossit sans bornes, caches non bornés) est important. Les fuites se voient dans le dashboard via la métrique CPU time per request qui dérive sur les Workers à longue durée de vie.
Sécurité et secrets en production
Workers fournit deux primitives pour les secrets. Les environment variables sont stockées en clair dans le tableau de bord et lisibles par toute personne avec accès au compte. Les secrets sont chiffrés au repos et ne peuvent être lus que par le Worker lui-même au runtime.
On définit un secret avec wrangler secret put. Une invite demande la valeur en local, qui n’apparaît jamais dans les logs.
npx wrangler secret put STRIPE_API_KEY
# Enter a secret value: sk_live_...
Côté Rust, on lit le secret via env.secret("STRIPE_API_KEY")?.to_string(). La distinction secret/variable n’est qu’une question de stockage : pour le code, l’API est identique. La règle pratique est de mettre en secret toute clé d’API tierce, tout token d’authentification et tout webhook secret, et de ne laisser en variable que des paramètres non sensibles (région, env name).
Pour la défense en profondeur, on ajoute un middleware d’authentification dans le router : vérification de signature HMAC sur les webhooks, validation JWT sur les routes API. worker expose req.headers() et l’on chaîne les vérifications avant l’appel métier.
Aller plus loin sur l’edge
Pour des cas d’usage statiques, l’ancien tutoriel Cloudflare Workers : tutoriel edge compute 2026 couvre le déploiement de Workers JavaScript et la configuration des routes custom. Côté frameworks frontend modernes, le tutoriel Déployer TanStack Start sur Cloudflare Workers : KV, D1 et R2 reprend le même socle avec un framework SSR sur Workers. Pour faire tourner le même code Rust hors edge, le tutoriel WASI et serveurs WebAssembly avec Wasmtime bascule la cible vers wasm32-wasip2. Le guide principal sur WebAssembly en production détaille les choix d’architecture qui justifient l’un ou l’autre runtime.
Ressources officielles
- developers.cloudflare.com/workers/languages/rust — page Rust officielle Workers
- github.com/cloudflare/workers-rs — code et exemples de la crate
worker - developers.cloudflare.com/workers/wrangler — référence Wrangler
- developers.cloudflare.com/kv — documentation Workers KV
- developers.cloudflare.com/d1 — documentation D1
- developers.cloudflare.com/r2 — documentation R2
- blog.cloudflare.com — Rust Workers reliable — panic recovery et exception handling
Cloudflare Workers + Rust + WebAssembly est probablement la combinaison la plus rentable pour déployer du code edge performant en 2026. Le coût d’apprentissage de la chaîne worker-build est compensé par des temps de réponse en dizaines de microsecondes et une facture stable. Pour aller au-delà du HTTP basique, le binding KV/D1/R2 documenté ici suffit à industrialiser une API métier complète.