Deploying a WASM Image-Resize Module to Cloudflare Workers
An exploration of whether a Rust + WASM image-resize handler fits inside Cloudflare Workers' binary-size, memory, and CPU ceilings before the POC runs.
A POST /resize?w=800&q=85 handler that takes a JPEG in the body and returns a resized JPEG is the simplest non-toy test of Cloudflare Workers + WASM as an image pipeline host. The design pushes three edge-platform limits at once: compiled binary size, per-isolate memory, and per-request CPU time. This post names which limit will bite first, in what order, and with what signal, before the POC runs.
The Job
An image-resize module is the honest stress test for CF Workers + WASM because it pushes all three hard limits at once: compiled binary size, per-request memory, and per-request CPU. This post walks the setup and names where each limit will show up before the POC runs. The reader leaves knowing whether to provision a Worker, a Lambda, or a managed image product for their pipeline.
This post operates inside WebAssembly 101's Bet 3: WASM as the portable compute primitive at the edge. That framing stays intact here; I will not re-derive it. The subject is the opposite direction, which is what happens when you load a real image codec into that primitive and send it body-in, body-out traffic.
The edge PoP terminates TLS and routes to a V8 isolate. The isolate, not a WASI runtime, hosts the WASM instance. Every byte of the request body, the decoded pixel buffer, and the encoded response sits inside a single 128 MB budget that the isolate shares across concurrent invocations.
What The Docs Promise
Binary size. Cloudflare publishes a 3 MB limit for the Free tier and 10 MB for the Paid tier after gzip, with a 64 MB ceiling before compression on both. The compressed number is the one that matters at deploy time, and wrangler deploy --outdir bundled/ --dry-run prints it as Total Upload: N KiB / gzip: M KiB. The uncompressed wasm file inside the bundle can be significantly larger than the gzip report suggests. See the Worker size limits page.
Memory per isolate. Each isolate can use up to 128 MB, including the JavaScript heap and WebAssembly allocations. Cloudflare documents this as per-isolate, not per-invocation; a single isolate handles many concurrent requests and their allocations all sit inside the same 128 MB envelope. Two concurrent 12-megapixel decodes will risk eviction long before a single synchronous one would. See Memory per isolate.
CPU time per request. The Free tier allows 10 ms of CPU per HTTP request, which disqualifies any non-trivial resize before the first pixel is encoded. The Paid tier moves this to a 30-second default, configurable up to 5 minutes. CPU time excludes network wait, so the whole resize cost lands squarely on this budget. See CPU time.
Startup time. A Worker must parse and execute its top-level code within 1 second, or wrangler rejects the deploy with error 10021. Large WASM bundles eat into this budget because the parser has to walk the whole module at startup. This is a deploy-time validator, not a runtime cost per invocation. See Worker startup time.
Isolate model. Workers runs on V8, the same engine Chromium and Node.js use. WASM is loaded via WebAssembly.instantiate() inside the V8 isolate; there is no WASI runtime underneath. Cloudflare's own docs describe WASI support as experimental, with only some syscalls implemented. For an image handler this is fine because the work is pure compute on byte buffers, no filesystem or network needed. See How Workers works and WebAssembly runtime APIs.
Setup
The workers-rs path is documented and the release line is active; v0.8.1 shipped on 17 April 2026. Target the wasm32-unknown-unknown triple, generate from the template, and let worker-build wire up the JS shim:
Add the image-processing dependencies. photon-rs currently publishes 0.3.3 on crates.io; pin that version. The image crate is pulled in transitively — photon-rs 0.3.3 pins image ^0.24.8 in its own Cargo.toml — so adding it as a direct dependency risks a double-version bundle. Drop the direct pin unless you need encoders that photon-rs does not expose:
The wrangler configuration scopes the Worker to Paid-tier behaviour from the start. The cpu_ms = 100 setting is deliberate; it surfaces the cost envelope early instead of hiding under the 30-second default:
No R2, no KV, no auth. The scope is body-in, body-out.
The Handler
The handler decodes the request body into a PhotonImage, computes the target height from the width query parameter to preserve aspect ratio, resizes with a Lanczos3 kernel, and re-encodes as JPEG at quality 85:
The .data() and .get_env() accessors on RouteContext were deprecated in earlier workers-rs releases in favour of direct field access; the example above avoids both. Before publishing a POC against this code, re-verify open_image_from_bytes, get_bytes_jpeg, and SamplingFilter::Lanczos3 against the photon-rs version pinned in Cargo.toml at the time of the run. Do not invent shim traits if a signature has drifted; fix the call site.
Where The Limits Will Bite
Future-tense throughout. These are predictions keyed to the docs; the POC will confirm or correct them.
Binary size
photon-rs pulls image ^0.24.8, imageproc ^0.23.0, palette, rusttype, perlin2d, and rand. The transitive graph is not small. After wasm-opt -Oz and gzip, the bundle will likely sit in the single-digit MB. Readers on the Free tier (3 MB) will not fit. Paid tier (10 MB) is realistic but tight; expect to trim features if you add any sibling crate. Confirm the actual gzip size with wrangler deploy --dry-run before committing to the approach.
Per-request memory
A 4000 x 3000 JPEG decodes to roughly 48 MB of RGBA8 (4000 x 3000 x 4). The resize scratch can briefly double that. The 128 MB ceiling is shared across concurrent invocations in the same isolate, so two simultaneous 12-megapixel decodes will risk eviction even if each one individually fits. For a user-upload endpoint, either enforce a maximum megapixel count at the edge or front the Worker with a queue that throttles concurrency per isolate.
CPU time
The Free tier's 10 ms CPU budget disqualifies the whole design. Lanczos3 resizing of a multi-megapixel image will not finish in 10 ms on any realistic isolate. On Paid, the 30-second default is plenty for a single image; starting at cpu_ms = 100 forces the handler to stay honest. When the POC runs and surfaces the actual per-image cost, raise the limit deliberately, not by default.
Cold start
The 1-second startup budget is a deploy-time validator. If the WASM bundle makes top-level parsing too slow, wrangler rejects the deploy with error 10021. Keep initialization inside the handler, not at module top level, and avoid loading lookup tables or font atlases on module init.
When CF Workers + WASM Is The Wrong Tool For This Job
Three shapes send the reader elsewhere.
Large source images or batches. If inputs routinely exceed roughly 8-10 megapixels, or if the job is batch-style over a folder, the 128 MB shared isolate will not stretch. S3 + Lambda with ephemeral /tmp storage handles single-image jobs with a bigger memory envelope. Fargate or ECS with a worker loop handles batches. Both pay a cold-start cost the Worker avoids, but both have memory ceilings that actually fit the workload.
Output format variety. AVIF, HEIC, and animated WebP encoders add substantial bytes to the bundle. Stacking them on top of photon-rs will blow through the 10 MB compressed ceiling. Cloudflare Images is the managed path for polyglot formats; a container behind a CDN is the self-hosted path.
Pipeline stages beyond resize. Face detection, OCR, or ML inference on the image is a different compute profile. Workers AI exposes models behind an HTTP surface, and a GPU task runner fits anything heavier. Trying to stuff inference into a CPU-bound Worker burns CPU budget faster than the WASM bundle can be trimmed.
Closing
For user-uploaded photos up to roughly 3000 px on the long edge, JPEG in and JPEG out, one image per request, Cloudflare Workers + workers-rs + photon-rs is a cheap path to a production resize endpoint. The boundary holds only on the Paid tier, for a single output format family, for one image per request. Outside those constraints, reach for S3 + Lambda, Fargate, or a managed image product before writing more Rust.
References
- Cloudflare Workers platform limits - Binary size, memory, CPU time, and startup limits on Free and Paid tiers.
- Cloudflare Workers Rust language guide - Official walkthrough for the
cargo generateplusworker-buildpath. - Cloudflare Workers WebAssembly reference -
WebAssembly.instantiate()model, WASI status, andwasm-optguidance. - How Workers Works - V8 isolate model and concurrency semantics on Cloudflare's edge.
- wrangler configuration reference -
wrangler.tomlandwrangler.jsoncincluding the[limits]block. - workers-rs repository - Active crate for Rust-authored Workers; v0.8.1 as of 17 April 2026.
- workers-rs release notes - Version history including v0.7
RouteContextdeprecations. - photon-rs API docs - Crate documentation for the native module used by the handler.
- photon-rs repository - Supported image formats and source for the rendering library.
- image crate documentation - Upstream image crate whose encoder and decoder feature flags shape the bundle size.