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
curlfor the module URL. - [ ] The loader code calling the streaming API.
- [ ] Optionally, a known-good copy of the
.wasmfile 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.
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.
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.htmlwith status 200. Check the first bytes. - Pre-compressed files without
Content-Encoding. The compiler sees compressed bytes. Declare the encoding. - Relative
fetchpaths. They resolve against the page, not the script. Useimport.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.
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.
Related
- Streaming instantiation vs ArrayBuffer instantiation — the two loading paths.
- Serving Wasm files with the right headers — the full header set.
- Versioning Wasm files with content hashes — avoiding stale references.
- Alerting on Wasm load failures — catching these in production.
← Back to Troubleshooting Common Wasm Errors