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.
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.
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.
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.
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;
unreachablealone 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
- Symbolicating Wasm stack traces in production — turning wasm-function offsets from a stripped binary back into names and lines.
- Reporting Wasm crashes to an error tracker — capturing traps and panics with context, and uploading debug files at build time.
- Measuring Wasm performance with real-user monitoring — timing download, compile and execution in the field with the Performance API.
- Emitting structured logs from server-side Wasm — JSON logs from a WASI guest through stdout or a host import, with request context.
- Tracing Wasm requests with OpenTelemetry — spans across the host and the guest so a slow request shows where the time went.
- Monitoring Wasm memory in production — sampling memory size from the field and alerting on growth before users hit the limit.
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.
Related
- Debugging and profiling Wasm modules — the local, lab-side tools.
- Errors and traps across the boundary — how failures cross into JavaScript.
- Memory profiling and leak detection — finding the cause of what monitoring detects.
- Serverless and edge deployment — where server-side guests run.
← Back to Production Wasm: Workloads & Deployment