Storing State for Wasm Edge Functions
This page answers one task: WebAssembly functions at the edge — Cloudflare Workers, Fastly Compute, Spin applications, Deno Deploy — handle requests statelessly, but your feature needs state: sessions, counters, user preferences, rate limits, cached computations, documents. You want to know which storage options edge platforms offer, how Wasm code reaches them, and which fits each kind of data.
Prerequisites
- [ ] An edge platform account and its CLI.
- [ ] A Wasm handler (Rust, Go, JavaScript with Wasm, or a component).
- [ ] A clear idea of each piece of state’s consistency and latency needs.
Why instance memory is not storage
An edge Wasm instance may live for one request (Fastly Compute, Spin create an instance per request) or for a while within an isolate (Cloudflare Workers keep isolates warm), but in neither case is its memory durable or shared: requests from the same user can hit different locations, different isolates, or a fresh instance. Anything kept in a global variable may vanish at any moment or exist in many divergent copies. Treat instance memory as a scratchpad and cache, never as the source of truth.
Platforms therefore provide external storage, each with different properties. The choice depends on how the data is read and written, how quickly writes must be visible everywhere, and how much coordination concurrent writers need.
Step 1 — use a KV store for read-heavy data
Key-value stores replicated to edge locations suit data read far more often than written: feature flags, configuration, user preferences, rendered fragments.
Reads are fast; writes propagate within seconds to a minute, so a value written in one location may be stale elsewhere briefly. In Rust on Cloudflare Workers
(worker crate, compiled to Wasm):
#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
let kv = env.kv("PREFS")?;
let user = user_id(&req)?;
let prefs = kv.get(&format!("prefs:{user}")).text().await?.unwrap_or_else(default_prefs);
Response::ok(render_with(prefs))
}
Spin applications use spin_sdk::key_value::Store::open_default(), backed by whatever the deployment configures; Fastly Compute offers KV stores through its
SDK. The API shape is the same: get, set, delete, sometimes list.
Step 2 — use coordinated state for counters and limits
Counters, rate limits, inventory, collaborative documents and anything with concurrent writers need consistency that eventually consistent KV cannot give. Cloudflare’s Durable Objects provide a single-threaded object per key, living in one location, with transactional storage; all requests for that key are routed to it, so increments and checks are serialised. Other platforms offer comparable patterns through external databases with transactions. Use them only where needed — routing to one location adds latency for distant users.
Step 3 — cache computed results at the edge
Expensive Wasm computations whose results depend only on request inputs — image transformations, rendered pages, API aggregations — can be cached in the edge cache keyed by those inputs (the platform’s Cache API). A cache hit skips the Wasm work entirely. Choose keys carefully (include every input that affects output, exclude everything else) and set TTLs to match how stale results may be.
Step 4 — use object storage and SQL where they fit
Large files belong in object storage (R2, S3-compatible stores) accessed through bindings or HTTP. Relational data with queries — orders, comments — fits edge SQL databases (such as D1, Turso/libSQL or regional Postgres with read replicas), which keep a primary for writes and replicas near users for reads. From Wasm, these are host bindings or HTTP calls; keep query counts per request low, since each is a network round trip.
Step 5 — reach storage through the host interface
Wasm handlers cannot open sockets or files freely; storage is reached through host APIs. On JavaScript-hosted platforms, Rust or Go code calls bindings exposed
through the platform’s SDK (wasm-bindgen glue under the hood). On WASI-based platforms, storage arrives through WASI interfaces or platform-specific WIT
interfaces — for example key-value interfaces in Spin and the wasi:keyvalue proposal — which makes handler code portable between hosts that implement the same
interfaces. Prefer those standard interfaces where available.
Caching inside instance memory, carefully
Keeping a small in-memory cache in a warm isolate (parsed configuration, a compiled regex set) is a useful optimisation, as long as correctness never depends on it. Give cached values short lifetimes, key them by version, and accept that each isolate has its own copy. Never accumulate per-user data in globals — it leaks memory and mixes users’ data across requests if handled carelessly.
Designing data for eventual consistency
Most edge storage is eventually consistent, and features work best when designed for it instead of fighting it. Write data in a form where staleness is harmless: versioned configuration where readers can tell old from new, append-only logs rather than in-place updates, and per-user records that only that user writes. When a user changes a setting, return the new value from the write request itself and have the client use it, rather than reading it back from KV immediately — the next read in another location may still see the old value for a few seconds. For values that must never go backwards, include a version or timestamp and have readers keep the newest they have seen. These patterns let read-heavy features enjoy fast global reads without visible glitches.
Testing storage-dependent handlers
Platform CLIs provide local emulations of their storage — local KV namespaces, simulated Durable Objects, local SQLite in place of edge SQL — which make it possible to test handlers end to end on a laptop. Emulators are usually strongly consistent, though, so they hide propagation delays; add tests that inject stale reads (a storage wrapper that returns the previous value for a short while) to check how features behave when replicas lag. For WASI-based handlers using standard interfaces, a test host that implements the key-value interface in memory lets unit tests run without any platform tooling.
Costs and limits
Edge storage is billed per operation and per stored byte, with limits on value sizes and operation rates. Wasm handlers that read several keys per request multiply costs at high traffic; combine related values into one key where they are read together, cache hot keys briefly in instance memory, and watch operation counts per request in metrics.
Expected output
User preferences are stored in an edge KV store and read in under 5 ms; rate limits use a coordinated object per API key with strongly consistent counters; transformed images are cached at the edge, with 92% of requests skipping the Wasm transform; originals live in object storage; and no correctness depends on data kept in instance memory.
Gotchas
- State in globals. It disappears or diverges. Use platform storage.
- Eventually consistent KV for counters. Increments get lost. Use coordinated state.
- Cache keys missing inputs. Wrong results served. Include every relevant input.
- Many storage round trips per request. Latency adds up. Batch and cache.
- Per-user data in instance caches. Leaks and cross-user mistakes. Keep instance caches generic.
- Testing only against strongly consistent emulators. Propagation delays go untested. Inject stale reads.
Performance note
With edge caching of transformed images, median latency fell from 85 ms (fetch original plus Wasm transform) to 9 ms for cache hits; KV reads took 2–5 ms at the edge, and coordinated-object calls took 20–60 ms depending on the user’s distance from the object’s location.
Frequently Asked Questions
Can a Wasm instance hold a database connection? Generally not across requests; use platform bindings or HTTP-based database APIs designed for edge use.
Is KV good for sessions? Yes for read-mostly session data; use short TTLs and accept brief staleness after writes.
How do I make handlers portable between platforms? Use standard interfaces such as WASI key-value where implemented, behind a small storage trait.
Where should rate limits live? In strongly consistent per-key state close to the protected resource.
How do I avoid users seeing stale values right after saving? Return the new value from the write request and use it on the client instead of reading back immediately from eventually consistent storage.
How can storage costs be kept down? Combine values read together into one key, cache hot keys briefly in instance memory, and monitor operations per request.
Can handlers be tested without the platform? Yes — implement the storage interface in memory in a test host, and use platform emulators for end-to-end checks.
How should values that must never go backwards be stored? With a version or timestamp, so readers can keep the newest value they have seen despite lagging replicas.
Related
- Deploying Wasm to Cloudflare Workers — the platform.
- Building HTTP services with Spin — Spin’s storage APIs.
- Running Wasm on Fastly Compute — another platform.
- Handling HTTP requests with WASI HTTP — the request interface.
← Back to Serverless & Edge Deployment