Falling Back to Server-Side Processing

This page answers one task: a feature runs in the browser with WebAssembly — image processing, document conversion, a calculation engine — but some clients cannot run it: old engines, devices without enough memory, enterprise browsers that block Wasm compilation, very slow phones. Instead of failing for them, you want to process their requests on a server, ideally with the same code, so results are identical either way.

Prerequisites

  • [ ] A Wasm module whose work can run on a server (in Node, Wasmtime, or natively from the same source).
  • [ ] A server endpoint and capacity planning for fallback traffic.
  • [ ] A privacy review: the fallback sends user data to your server, the client path does not.

When the client cannot or should not run the module

Clients fail to run modules for several reasons. Feature gaps: the engine lacks a feature even your baseline build needs. Policy: some enterprise environments set a Content Security Policy or browser policy that blocks WebAssembly compilation. Resources: the device lacks the memory for large inputs, or the work would take too long on a slow CPU and drain battery. Load failures: the module failed to download or compile for reasons the client cannot fix. In each case, a server can do the work and return the result.

Server fallback is not free: it costs server capacity, adds network latency, and — most importantly — sends user data off the device, which may conflict with the reason you chose client-side processing in the first place. Make it explicit, and in privacy-sensitive products, ask the user before using it.

Choosing between client and server processing The client checks whether it can run the module: features validate, the module loads, the input fits memory and time budgets. If all checks pass, it processes locally. Otherwise, with the user's consent where required, it uploads the input to a server running the same module and displays the returned result. feature + policy checks validate, CSP load module catch failures input within budget? size, device class process locally default path server fallback same module, with consent

Step 1 — define one interface with two implementations

Hide the choice behind a single function with the same inputs and outputs:

interface Converter { convert(input: Blob, opts: Options): Promise<Blob>; }

const local: Converter = { convert: (input, opts) => wasmConvert(input, opts) };
const remote: Converter = {
  async convert(input, opts) {
    const res = await fetch(`/api/convert?${new URLSearchParams(opts as any)}`, { method: "POST", body: input });
    if (!res.ok) throw new Error(`server conversion failed: ${res.status}`);
    return res.blob();
  },
};

UI code calls converter.convert() without knowing where the work happens.

Step 2 — decide which implementation to use

Pick the implementation at startup and per request:

async function chooseConverter(input: Blob): Promise<Converter> {
  if (!(await canRunWasm())) return remote;                  // feature probes + successful load
  if (input.size > localLimitFor(deviceClass())) return remote;   // too big for this device
  return local;
}

canRunWasm combines feature detection with an actual attempt to compile the module (catching CompileError, including CSP blocks). The size limit protects low-memory devices from inputs that would crash the tab. If the local attempt fails mid-way (a trap, a RangeError on memory growth), retry once on the server.

Step 3 — run the same module on the server

For identical results, run the same Wasm module server-side — in Node with the module’s Node-compatible glue, or in Wasmtime if it is a WASI module — or compile the same source natively. Using the identical artefact avoids subtle differences between a client build and a separately maintained server implementation. Apply server-side limits (time, memory, input size) because fallback requests come from clients that could not handle the input — often the largest inputs.

Same module on the server versus a separate server implementation Running the same Wasm module on the server guarantees identical results and one codebase, with Wasm runtime overhead on the server. A separate server implementation can be faster or use native libraries, but results may differ and two codebases must be maintained. same Wasm module identical results one codebase Node or Wasmtime host default choice separate implementation native libraries, speed results may differ two codebases only if needed

The client path keeps data on the device; the fallback does not. In products that promise local processing, tell users before falling back (“This device can’t process the file locally. Upload it for server processing?”) and respect a refusal. Encrypt in transit, delete inputs and outputs promptly after returning results, and do not log content. Document the fallback in your privacy policy.

Step 5 — measure fallback usage

Record every fallback with its reason (features, policy, size, failure) and device class. The data shows how much server capacity the fallback needs, which reasons dominate (a CSP issue at one enterprise customer, a memory limit on a popular phone), and whether improving the client — a lower baseline build, chunked processing for large inputs — would remove most fallbacks.

Latency and user experience

Server processing adds upload and download time; for large files on slow connections it can be slower than local processing would have been on a capable device. Show progress for upload and processing separately, and consider processing a reduced version locally for preview while the server produces the full result.

Partial fallbacks

Fallback does not have to be all-or-nothing. A document converter might parse and preview locally — cheap, and enough for the user to check the file — and send only the expensive final rendering to the server when the device cannot handle it. An image editor might apply filters locally at preview resolution and request the full-resolution export from the server. Splitting the pipeline this way keeps the interactive part responsive and private, limits what is uploaded, and reduces server load compared with sending everything. Design the module’s API in stages (parse, transform, render) so each stage can run on either side, with a well-defined intermediate format between them — ideally the same format the module already uses internally, serialised compactly.

Capacity planning and abuse

A public fallback endpoint is a free processing service unless protected. Require the same authentication as the rest of the application, rate-limit per user, cap input sizes and processing time, and queue work when demand spikes rather than scaling without bound. Estimate capacity from measured fallback rates: if 3% of conversions fall back and each takes two CPU-seconds on the server, peak traffic tells you how many workers you need. Watch for changes — a browser update that breaks your baseline build, or a new enterprise customer with a strict policy, can multiply fallback traffic overnight, which is another reason to alert on the fallback rate per release.

Keeping both paths tested

The server path runs for a small share of users, so its bugs surface slowly. Run the same test suite against both implementations of the interface in CI — local in a headless browser, remote against a test server — and compare outputs for identical inputs. That catches version skew between the module deployed to clients and the one running on the server, which happens easily when the two are deployed by different pipelines.

Expected output

The converter runs locally for 96% of requests; 3% fall back for input size on low-memory phones and 1% for policy blocks at two enterprise customers; the server runs the identical module in Node with a 30-second limit; users of the privacy-focused plan are asked before any upload; and a dashboard tracks fallback reasons per release.

Gotchas

  • Silent uploads in privacy-focused products. Ask first.
  • Separate server logic. Results drift. Run the same module.
  • No server-side limits. Fallback traffic brings the biggest inputs. Limit time and memory.
  • Feature detection without a load attempt. CSP blocks are missed. Try compiling.
  • Unmeasured fallbacks. Capacity surprises. Record reasons.
  • Unprotected fallback endpoints. They become free compute for anyone. Authenticate and rate-limit.

Performance note

For a 40 MB document on a mid-range phone over 4G, local conversion took 6.1 s; server fallback took 9.4 s, of which 7.2 s was upload — a reminder that the fallback is for clients that cannot process locally, not a speed upgrade.

Converting a 40 MB document on a mid-range phone Seconds to convert a 40 MB document locally with Wasm on a mid-range phone and through the server fallback over a 4G connection, including upload and download. seconds to result local Wasm conversion 6.1 s server fallback (incl. upload) 9.4 s

Frequently Asked Questions

Can the fallback run at the edge? Yes — edge functions with Wasm suit small inputs; large files belong in regional services with object storage.

Should I detect slow devices and fall back? Only with care; device class is a weak signal. Prefer size limits and user choice.

How do I test the fallback? Force it with a test flag, and simulate policy blocks with a CSP lacking 'wasm-unsafe-eval'.

What if the server also fails? Show a clear error with the reason; never leave the user without feedback.

Can part of the work stay on the device? Yes — split the pipeline into stages so previews and parsing run locally and only heavy final steps go to the server.

How should fallback capacity be estimated? From measured fallback rates and per-request server cost at peak traffic, with alerts when the rate changes per release.

How do I keep client and server modules in sync? Run the same tests against both implementations and compare outputs, and deploy both from one pipeline.

Should the fallback be the default for slow devices? Rarely — local processing is usually still acceptable; use size limits and let users choose.

Should failures on the local path retry remotely? Yes, once, for traps or memory errors on large inputs — and record the reason.

← Back to Polyfill Alternatives & Fallbacks