Fixing WebAssembly.instantiateStreaming Failures

This page answers one task: WebAssembly.instantiateStreaming (or compileStreaming) rejects — with a TypeError, a CompileError such as expected magic word 00 61 73 6d, found 3c 21 44 4f, or a network error — and the MIME type is already correct, so you need to find what else is wrong between the server and the compiler.

Prerequisites

  • [ ] DevTools access to the failing page, or curl for the module URL.
  • [ ] The loader code calling the streaming API.
  • [ ] Optionally, a known-good copy of the .wasm file to compare bytes against.

The path the bytes take

A streaming instantiation succeeds only if every link in a chain holds: the request reaches the right file, the server or CDN returns it with a success status, cross-origin rules allow the page to read it, content encoding is declared and decoded correctly, nothing in between (a service worker, a proxy) alters it, the body arrives complete, and the bytes form a valid module for this browser. The MIME type is one link, covered in fixing incorrect response MIME type errors. The error message names the symptom at the point the chain broke, which is often several steps downstream of the cause — a 404 page appears to the compiler as a module that starts with <!DOCTYPE.

The chain between the server and the compiler The request must resolve to the right file, return a success status, pass CORS, carry correct content encoding, survive any service worker or proxy, arrive complete, and contain a valid module. A break at any link surfaces as a fetch, type or compile error at the end. right URL + 200 not a 404 page CORS allowed cross-origin reads encoding correct br / gzip declared complete body no truncation valid module features supported

Step 1 — read the first four bytes

The quickest diagnostic is the compile error’s “found” bytes, or the first bytes of the response body. A valid module starts with 00 61 73 6d (\0asm). Anything else identifies the problem:

3c 21 44 4f  "<!DO" → an HTML page: 404 fallback, login page or error page
7b 22 ...    "{\""   → JSON: an API error response
1f 8b 08 ..          → gzip data that was not decoded (Content-Encoding missing)
ce b2 cf 81 / 0b ..  → Brotli data that was not decoded
curl -s https://example.com/app.wasm | head -c 8 | xxd
# 00000000: 3c21 444f 4354 5950  <!DOCTYP     ← not the module

For HTML, check the URL in the Network panel: single-page-app hosts often return index.html with status 200 for any unknown path, so a wrong relative path looks like success until the compiler sees it.

Step 2 — fix paths that resolve differently in production

Relative module URLs resolve against the document or the script, depending on how they are written. fetch("app.wasm") resolves against the page URL, so it breaks on nested routes like /docs/page/. new URL("./app.wasm", import.meta.url) resolves against the JavaScript file, which is what you want for bundled code. Base paths, CDN prefixes and content-hashed file names add further ways to point at a file that does not exist. The robust pattern is described in resolving Wasm URLs with import.meta.url.

Step 3 — fix CORS and compression

When the module is on another origin (a CDN), the response needs Access-Control-Allow-Origin for the page’s origin — the console shows a CORS error and the streaming call rejects with TypeError: Failed to fetch. Under Cross-Origin-Embedder-Policy: require-corp, it also needs Cross-Origin-Resource-Policy: cross-origin (or CORS). For compression, the rule is that the bytes and the header must agree: a Brotli file served with Content-Encoding: br is decoded by the browser; the same file served without the header reaches the compiler still compressed. Pre-compressed files uploaded to object storage are the usual culprit. Double compression — a pre-compressed file that the CDN compresses again — produces the same symptom.

First bytes of the response and what they mean A valid module starts with 00 61 73 6d. An HTML document begins with a less-than sign, JSON with a brace, gzip data with 1f 8b, and undecoded Brotli with no fixed signature but non-module bytes. Each points to a different fix. first bytes what arrived fix 00 61 73 6d a Wasm module look at features or truncation 3c 21 44 4f HTML page fix the URL or route 7b 22 JSON error fix the API or auth route 1f 8b gzip not decoded add Content-Encoding other binary Brotli not decoded add Content-Encoding br

Step 4 — rule out truncation and service workers

A module that is cut short fails with CompileError: ... unexpected end of section or function or similar. Compare the response size with the file’s size; a proxy with a body limit, an interrupted deployment or a range request handled incorrectly can deliver partial files. Service workers are another source of surprises: a worker that caches responses can serve an old module, an old Content-Type, or an opaque response it cannot read. Bypass the service worker in DevTools (Application → Service Workers → “Bypass for network”) to confirm, then fix its caching strategy, as described in caching Wasm with a service worker.

Step 5 — check features when the bytes are valid

If the bytes start with 00 61 73 6d, are complete, and compilation still fails, the module probably uses a feature this browser lacks — SIMD, threads, exception handling, GC, tail calls — or a newer encoding of one. The error names the offset and often the opcode. Validate the file with wasm-tools validate --features=… to see which feature is involved, and either ship a build for older engines or feature-detect before loading, as covered in detecting proposal support at runtime.

Building loaders that report useful errors

Most of these failures are hard to diagnose from user reports because the loader throws the engine’s message and nothing else. Wrap the streaming call so that, on failure, it records what actually arrived: the URL after resolution, the status, the Content-Type, Content-Encoding and Content-Length headers, and the first eight bytes of the body (fetched again with arrayBuffer() only on the error path). Send that with the error to your tracker. A report that says “status 200, text/html, 3c 21 44 4f” is solved in seconds; one that says “CompileError” can take days. Keep the extra fetch on the failure path only, so successful loads pay nothing. Tag the report with the deployed build version too: many streaming failures appear for a few minutes after a deployment, when an old HTML page references hashed files that the new deployment removed, and correlating reports with deploy times makes that pattern obvious. Keeping the previous release’s assets available for a while after each deployment eliminates that whole class of errors.

Testing the failure paths

Each failure mode can be reproduced deliberately in a test environment: serve the module with a wrong path (HTML fallback), without Content-Encoding for a Brotli file, truncated by a few bytes, and with a missing CORS header from a second origin. Playwright can intercept requests and return each variant. Assert that the loader fails with the improved error message and falls back sensibly where you designed it to. These tests take minutes to write and guard against regressions in hosting configuration, which tend to happen during infrastructure changes nobody connects with the WebAssembly feature.

Authentication and redirects

Two more cases look puzzling at first. If the module sits behind authentication — a staging environment with basic auth, an app that requires a session cookie for static assets — an unauthenticated request receives a login page or a 401 with an HTML body, and the compiler reports the familiar 3c 21 bytes. Make sure the request carries credentials (fetch(url, { credentials: "include" }) for cross-origin cookies) or serve static assets without authentication. Redirects are usually harmless, since fetch follows them, but a redirect to another origin turns a same-origin request into a cross-origin one, which then needs CORS headers on the final response; and a redirect loop surfaces as a generic network error. The Network panel’s “Initiator” and “Redirect” columns show the full chain.

Expected output

The loader’s error report for a broken deployment reads:

instantiateStreaming failed: https://cdn.example.com/app.3f9a.wasm
  status=200 type=text/html encoding=none first-bytes=3c21444f

That points directly at a path problem; after fixing the base path, the module streams and compiles normally.

Gotchas

  • Single-page-app fallbacks. Missing files return index.html with status 200. Check the first bytes.
  • Pre-compressed files without Content-Encoding. The compiler sees compressed bytes. Declare the encoding.
  • Relative fetch paths. They resolve against the page, not the script. Use import.meta.url.
  • Stale service-worker caches. Bypass the worker to confirm, then version the cache.
  • Removed assets after deploys. Old pages request old hashed files. Keep previous assets for a while.

Performance note

Recording diagnostic details only on the failure path cost nothing on successful loads. In one production incident, the enriched reports reduced time to diagnosis from hours to minutes: 94% of failures showed text/html responses from a base-path change.

Causes of streaming failures in one week of production reports Share of instantiateStreaming failures by cause in one week of enriched error reports, showing HTML fallbacks from wrong paths, undecoded compression, CORS rejections, truncated downloads and unsupported features. share of failures (%) HTML fallback (wrong path) 61 % undecoded compression 14 % CORS rejected 11 % truncated download 8 % unsupported feature 6 %

Frequently Asked Questions

Why does the error say “magic word” when my file is fine? The browser received different bytes from the file you checked — usually an HTML page or still-compressed data.

Can opaque responses be compiled? No. Cross-origin requests without CORS produce opaque responses, which the streaming API rejects.

Does fetch with cache: "no-store" help? Only for diagnosing stale caches. It slows normal loads; fix the caching instead.

Is the fallback to arrayBuffer() a fix? It only helps for MIME-type problems. HTML, CORS and truncation fail the same way on both paths.

Do these errors happen in Node? Yes, when streaming from fetch in Node; reading files from disk avoids most of them.

Why does the module load locally but not on staging? Staging often adds authentication, a different base path or a CDN. Compare the first bytes and headers of both responses.

← Back to Troubleshooting Common Wasm Errors