Skip to content

What Happens When a Browser Doesn't Support WebGPU

10 min read · updated August 11, 2026

The bug report says the page is slow, not broken. Somewhere in the console is a line beginning removing requested execution provider "webgpu" from session options, and everything downstream of that line is a model running on the CPU through WebAssembly while your code still believes it asked for the GPU.

The symptom and the exact strings

There are three distinct strings and they mean three different things, so it is worth matching the one you actually have.

  • removing requested execution provider "webgpu" from session options because it is not available: — a console.warn, not an error. ONNX Runtime Web could not initialise the WebGPU backend, found another one that worked, and continued. This is the silent slowdown.
  • no available backend found. ERR: [webgpu] ... — a thrown Error. Every backend you asked for failed. You see this instead of the warning when you passed executionProviders: ['webgpu'] and nothing else.
  • WebGPU is not supported in your current environment, but it is necessary to run the WebLLM engine — WebLLM’s WebGPUNotAvailableError. WebLLM has no CPU backend, so it fails loudly rather than degrading. Its sibling, Cannot find WebGPU in the environment, is WebGPUNotFoundError and means navigator.gpu was missing entirely.

The first two come from ONNX Runtime’s own backend resolution code, which is worth reading once because it explains the behaviour exactly: it tries each requested backend in order, keeps the first one that initialises, throws only if none did, and emits a console.warn for each backend that was explicitly requested and failed.

Why it is silent

The design is defensible and it is the source of the problem. An execution provider list is a preference list, not a requirement, in every ONNX Runtime binding. Falling back is the behaviour that keeps an application working on hardware the developer did not anticipate, and it is the same behaviour that makes a CUDA session quietly run on CPU on a machine without a GPU.

What makes it worse in a browser is that you never see the console. The fallback happens on a stranger’s machine, the page still produces correct output, and the only signal is a latency distribution with a long tail you cannot explain. Nothing in the session object after creation prominently announces “you are on WASM” — you have to go and ask.

Which browsers actually have it

“All major browsers support WebGPU” is true and misleading, because the gaps are per-platform rather than per-browser. From MDN’s browser-compat-data for the GPU interface (mdn/browser-compat-data), at the time of writing:

  • Chrome and Edge: from 113 on ChromeOS, macOS and Windows. Linux only arrived in Chrome 144, and there only on Intel Gen12 and newer GPUs.
  • Chrome on Android: from 121.
  • Firefox: from 141 on Windows; from 145 on macOS Tahoe on Apple silicon, and from 147 on older macOS on Apple silicon. Not supported on macOS with Intel CPUs, and not supported on Linux at all. Also not in service workers.
  • Firefox for Android: not supported.
  • Safari: from 26, which carries to iOS and iPadOS on the same version.

Two populations follow from that list and neither is small: Linux desktop users on Firefox or on a non-Intel GPU, and Android users on Firefox. If your analytics show either, you have a WASM path in production whether you designed one or not.

This matrix changed twice in the year before writing and will change again. Treat the shape of it — gaps are per-platform — as the durable part, and re-check the versions.

Diagnosing it in three lines

There are two separate failure points and they need separate checks. navigator.gpu being undefined means the browser has no WebGPU at all, or the page is not in a secure context. requestAdapter() resolving to null means the browser has WebGPU but could not give you an adapter — a blocklisted driver, a virtualised or headless environment, or no compatible GPU. MDN is explicit that the promise resolves to null rather than rejecting, which is why a try/catch around it catches nothing (MDN: GPU.requestAdapter()).

export async function webgpuStatus() {
  if (!("gpu" in navigator)) return "no-api";        // or insecure context
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) return "no-adapter";                 // driver blocklist, VM, headless
  const f16 = adapter.features.has("shader-f16");
  return f16 ? "ready-f16" : "ready-no-f16";
}

// Decide explicitly rather than letting the runtime decide for you.
const status = await webgpuStatus();
if (status.startsWith("ready")) {
  session = await ort.InferenceSession.create(url, { executionProviders: ["webgpu"] });
} else {
  reportToTelemetry("webgpu_unavailable", status);
  // ...and now make a decision, rather than loading a 2 GB model onto the CPU.
}

Send that status string to your telemetry. It is the difference between “some users say it is slow” and “11% of sessions have no adapter, and they are all on Linux”. The shader-f16 check is there because half of WebLLM’s prebuilt models require that feature, as described in the WebLLM tutorial.

What the WASM path really costs

The fallback is not nothing. ONNX Runtime Web’s WebAssembly backend uses SIMD and, where it can, multiple threads. But the ceiling is set by what a CPU is: a handful of cores, each executing 128-bit SIMD lanes, against a GPU offering thousands of concurrently scheduled arithmetic lanes. That is a structural gap of orders of magnitude on the dense matrix multiplications that dominate a transformer, and no amount of tuning closes it.

Worse, the threading half is conditional. ONNX Runtime Web’s documentation states that multi-threading is only enabled when the browser supports WebAssembly multi-threading and crossOriginIsolated mode is enabled (ONNX Runtime Web environment flags). Cross-origin isolation requires serving the page with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, which breaks third-party embeds that are not CORP-enabled. So the common production case is single-threaded WASM: the slow path, without the one mitigation it has.

Two settings are worth knowing regardless. env.wasm.numThreads controls thread count, with 0 meaning “decide for me” and 1 forcing single-threaded. env.wasm.proxy moves the work to a Web Worker, which does not make it faster but stops it freezing the page — which, if you are stuck on this path, is the difference between slow and broken.