Observability & Error Reporting

WebAssembly that works on a developer’s machine still has to be understood in production. Users run it on devices you do not own, with inputs you did not test, under memory and network conditions you cannot reproduce on demand. When something goes wrong, the defaults are unhelpful: a trap surfaces as RuntimeError: unreachable, stack frames read wasm-function[1234]:0x5a3f, memory problems show up as tabs that silently reload on phones, and a server-side guest is a black box inside a request that was simply “slow”. Observability is the work of turning those opaque signals into answers.

This topic covers the four signals that matter for Wasm in production — errors, performance, logs and traces, and memory — and how to collect each one in the browser and on the server without shipping debug information to users or leaking their data. The individual guides go deep on each signal; this page explains how they fit together and walks through a minimal setup that covers all of them.

Prerequisites

  • [ ] A Wasm module running in production or staging, loaded through code you control.
  • [ ] An error tracker, a real-user monitoring endpoint or analytics pipeline, and — for server-side guests — a log and metrics stack.
  • [ ] A build pipeline you can extend to keep per-release artefacts such as debug files.

Why Wasm needs specific instrumentation

Most observability tooling was designed for JavaScript or native services, and it mostly works for WebAssembly — but with gaps at the boundary. Error trackers capture exceptions, but a Rust panic’s message goes to the console and is lost before the trap reaches them. JavaScript source maps do not cover Wasm frames; those need DWARF or name sections, kept per build. Browser performance APIs time scripts and resources, but not compilation or instantiation of a module, which must be marked explicitly. Heap profilers and memory APIs focus on the JavaScript heap, while linear memory lives outside it and never shrinks. And on the server, a guest’s logs and timing are only visible if the host collects them, because the sandbox gives the guest no direct way out.

Each gap has a straightforward fix, and the fixes share one idea: instrument at the boundary — in the loader, the wrapper, and the host — where your code sees both sides.

The observability signals for Wasm in production Errors are captured at the wrapper with panic messages and symbolicated using per-build debug files. Performance is measured with marks around download, compile, instantiate and calls. Logs and traces leave server guests through host imports. Memory is sampled from linear memory and allocator statistics. errors panic hook + wrapper capture + symbolication performance marks for load, compile, instantiate, calls logs and traces host imports, trace context into guests memory linear memory size + allocator live bytes boundary instrumentation loader, wrapper, host

Errors: from traps to actionable reports

A useful Wasm crash report has four parts: the panic or abort message, a symbolicated stack, the build it came from, and context about the operation that failed. Getting the message requires a panic hook (Rust) or an abort handler (Emscripten) that records it before the trap. Getting a symbolicated stack requires keeping debug information for every release — the shipped binary is stripped, but a debug copy with identical code is stored under a build id — and uploading it to the error tracker. Grouping requires fingerprinting by panic location, since every Rust panic otherwise looks like the same unreachable error. The two guides symbolicating Wasm stack traces in production and reporting Wasm crashes to an error tracker cover the build side and the runtime side.

The path of a production crash report A panic hook records the message, the wrapper catches the trap and adds build id and operation context, the error tracker receives the report, matches the build's uploaded debug file, symbolicates the Wasm frames and groups the issue by panic location. panic hook message recorded wrapper catches trap + build id, operation error tracker ingest report debug file by build id uploaded in CI symbolicated, grouped issue actionable

Performance: what users actually wait for

Field performance for Wasm splits into phases with different fixes: download (size, compression, caching), compilation (size, streaming, code caching), instantiation (start-up work), and the operations users trigger, whose first call is often slower than later ones. Mark each phase with performance.mark, use Resource Timing for the network part, sample hot operations rather than timing every call, and report percentiles segmented by device and build. Connecting these numbers to Web Vitals shows whether Wasm is on the critical path at all. The full method is in measuring Wasm performance with real-user monitoring.

Where a typical Wasm feature's first-use time goes on a mid-range phone Milliseconds spent in each phase of a feature's first use for an uncached visit on a mid-range phone: download, compilation overlapping the download, instantiation, and the first call. ms on first use (uncached, mid-range phone) download (1.1 MB Brotli) 620 ms compile beyond download 140 ms instantiate 35 ms first call 180 ms

Logs and traces for server-side guests

On the server, guests run inside a host — wasmtime, Spin, an Extism-based plugin host — and can only emit telemetry through interfaces the host provides. The portable channel is JSON lines on WASI stdout and stderr, which every host captures. The better channel, when you own the host, is a logging import through which the host formats entries, adds trustworthy identity (tenant, plugin, request id), and enforces budgets so one guest cannot flood the pipeline. Tracing follows the same pattern: the host creates spans for instantiation, guest calls and every outbound request it mediates, and passes trace context into the guest so guest-created spans join the request’s trace. See emitting structured logs from server-side Wasm and tracing Wasm requests with OpenTelemetry.

Memory: the signal unique to Wasm

Linear memory grows and never shrinks, leaks inside the module are invisible to JavaScript tools, and on phones excessive memory ends sessions without an error. Monitoring it means sampling linear memory size and allocator live bytes over sessions, reporting peaks and growth slopes per release, recording out-of-memory failures as events, and inferring tab kills from heartbeats. Server hosts read each instance’s memory directly after every call. The details are in monitoring Wasm memory in production.

Where each signal is collected In the browser, errors are captured in the wrapper, performance with marks and Resource Timing, and memory by sampling linear memory. On the server, the host captures traps, times instantiation and calls in spans, collects logs through imports or stdio, and reads instance memory after each call. signal browser server host errors wrapper + panic hook catch traps around guest calls performance marks + Resource Timing spans for instantiate and call logs console / beacon stdio or log import memory sample memory.buffer read instance memory per call

Workers, iframes and other execution contexts

Much Wasm runs outside the page’s main thread: in dedicated workers for heavy computation, in audio worklets, in service workers, in sandboxed iframes hosting plugins. Each context has its own global error handlers, its own performance timeline and its own memory, and none of them report to the page automatically. Treat every context as a small service that must forward its telemetry. Initialise the error tracker’s SDK inside workers where it supports worker scope, or catch errors and post them to the page with the same enrichment. Collect performance marks inside the worker and send them back with results, or beacon them directly, since fetch and sendBeacon work in worker scope. Have each worker sample its own module’s memory and include it in its messages. Tag every report with the context it came from — “image-worker”, “search-worker”, “plugin:word-count” — so dashboards can separate them. Audio worklets are the exception: they must never allocate or block, so they should only increment counters in shared memory that another thread reads and reports.

Release health and safe rollouts

Observability pays off most at release time. Every report carries the build id, so each release accumulates its own crash-free session rate, load-time percentiles and memory peaks within hours of going out. Roll out new Wasm builds gradually — to a fraction of users, or behind a flag — and compare the new release’s numbers with the previous one’s before widening the rollout. Typical regressions show up clearly: a toolchain upgrade that adds 300 KB to the binary moves download percentiles; an optimisation-level change moves compile time on slow phones; a new code path moves crash rates or memory slopes. Keep the previous build deployable, so rollback is a configuration change rather than a rebuild, and keep its debug files, since users with cached copies will keep reporting from it for days. Annotate dashboards with deploys and toolchain changes, so the question “what changed?” has an answer on the chart itself.

What a server host sees without guest changes

On the server, the host is in a privileged position: it compiles, instantiates and calls every guest, and mediates every import. That means a great deal of telemetry needs no cooperation from guest authors at all. For each invocation the host can record compile-cache hits, instantiation time, call duration, fuel consumed, memory size after the call, whether the call trapped and with which trap code, and every outbound request with its destination, status and latency. Attach the guest’s identity and version from the host’s own registry, not from anything the guest reports. These host-side signals answer most operational questions — which guest is slow, which one is growing, which dependency is failing — and they work identically for guests written in Rust, Go, JavaScript or anything else. Guest-side logs and spans then add domain detail on top, where authors choose to provide it.

Observability in development and CI

The same instrumentation is valuable before production. Run the loader’s marks and the memory sampler in end-to-end tests, and fail the build when a change pushes load time, first-call time or peak memory beyond a budget on a throttled device profile. Include a test that triggers a deliberate panic in a staging build and asserts that the resulting report contains a symbolicated frame, which proves the debug-file pipeline works on every release rather than only when someone checks. For server guests, run a load test with tracing at 100% sampling and confirm the overhead stays within budget. Treating telemetry as a tested feature means it is ready on the day it is needed.

Privacy and data minimisation

Telemetry from WebAssembly modules can leak user data in ways that are easy to miss. Panic messages often include formatted values — a string that failed to parse, a file name, an email address in a validation error. Breadcrumbs may record arguments. Logs from server guests may contain request bodies. Apply the same rules everywhere: report sizes, types, counts, operations and build ids; scrub messages through a filter before sending; never attach inputs or memory dumps automatically; and keep retention as short as the purpose allows. Make scrubbing part of the shared reporting code rather than relying on every developer to remember it, and test it with deliberately sensitive values in staging.

Alerting without noise

Collecting signals is only half the work; someone has to be told when they change. Alert on rates and trends rather than single events: the crash-free session rate for the newest release dropping below the previous release’s, the 95th-percentile load time rising by a fixed margin, the share of sessions with growing memory increasing, or out-of-memory events on any device class exceeding a small threshold. Route Wasm alerts to the team that owns the module, with links to the release, the dashboard and the top crash groups, so the first responder starts from context rather than from a raw number. Review alert thresholds after each incident: an alert that fired too late, or one that fires weekly without action, both need tuning.

Choosing tools

Most teams can cover Wasm observability with tools they already have. Error trackers such as Sentry support WebAssembly debug files and symbolication. Real-user monitoring products accept custom metrics for load phases and operations, or a simple endpoint feeding a time-series database works just as well. OpenTelemetry covers traces, metrics and logs for server hosts, and several Wasm hosts integrate with it directly. What is specific to Wasm is not the tooling but the instrumentation at the boundary — panic hooks, build ids, debug files, marks around compilation, host imports for guest telemetry — which is the subject of the guides below.

Step-by-step: a minimal observability setup

The following setup covers all four signals for a browser feature in about a hundred lines of code, and is a good starting point before adopting the deeper techniques in each guide.

1. Stamp the build. Inject a build id into the loader at build time — a content hash of the shipped .wasm — and keep the debug copy of the binary under that id.

2. Wrap the module. Route every call through a wrapper that records breadcrumbs, catches WebAssembly.RuntimeError, attaches the last panic message and the build id, reports to the error tracker, and resets the instance.

export async function call(op, fn) {
  const t0 = sampled() ? performance.now() : 0;
  try {
    return fn(await engine());
  } catch (err) {
    if (err instanceof WebAssembly.RuntimeError) {
      reportCrash(err, { op, build: WASM_BUILD_ID, panic: lastPanic });
      resetEngine();
    }
    throw err;
  } finally {
    if (t0) recordTiming(op, performance.now() - t0);
  }
}

3. Mark loading. Put marks around compileStreaming and instantiate, and read Resource Timing for the .wasm file.

4. Sample memory. Every 30 seconds, record memory.buffer.byteLength and the allocator’s live bytes in a bounded array.

5. Send one beacon. On visibilitychange to hidden, send a single beacon with load timings, sampled call timings by operation, the memory summary, the build id and the device class.

6. Upload debug files in CI. Fail the release if the upload fails.

With these six steps, a dashboard can show crash-free sessions per release, load and call percentiles, and memory peaks — and every crash arrives with a readable stack.

Gotchas and failure modes

  • Panics without messages. Install a panic hook; unreachable alone says little.
  • No debug files for a release. Its crash reports stay unreadable. Upload in CI and fail on error.
  • Averages instead of percentiles. The slow tail disappears from view.
  • Workers left out. Errors and timings in workers do not reach page handlers. Forward them.
  • Guest-controlled identity in logs. Let the host attach tenant and plugin ids.
  • User data in telemetry. Report sizes, types and operations, never contents.
  • Watching only the JavaScript heap. Linear memory is separate and never shrinks.

Verification

Test the pipeline before you need it. Deploy a staging build with a deliberate panic behind a debug flag, trigger it, and confirm the error tracker shows the panic message, a symbolicated stack with file and line, the build id and the operation. Load the feature on a throttled phone profile and confirm the load and first-call timings arrive with sensible values. Run a scripted session that leaks a little memory per operation and confirm the live-bytes slope appears in the dashboard. For server guests, send a request with a traceparent header and confirm the trace shows host and guest spans and the outbound call, and that the logs carry the same trace id.

Guides in this topic

Frequently Asked Questions

Do I need all four signals from day one? Start with errors and load performance; they catch the most common production problems. Add memory monitoring for memory-heavy features and tracing when guests run on the server.

Does observability slow the module down? Done as described — sampling, summaries, host-side spans — the overhead is well under one percent.

Can I use the same tools for browser and server Wasm? Error trackers and OpenTelemetry cover both; the collection points differ, as the table above shows.

What about privacy regulations? Collect only technical data — sizes, timings, build ids, device classes — and treat any message that could contain user content as sensitive.

Should debug builds ever be shipped to users? No. Ship stripped binaries and symbolicate on the server with stored debug files.

How do I know telemetry itself is working? Alert on its absence: a release that reports no Wasm timings or no crash-free-session data has a broken pipeline, not a perfect build.

← Back to Production Wasm: Workloads & Deployment