Post-mortem debugging for Wasm on Cloudflare Workers

Debugging remains one of the clearest indicators of a language ecosystem's maturity. For WebAssembly (Wasm), which has gained momentum as a compilation target for Rust, C, and C++ in serverless and web environments, crash analysis has largely relied on logging. Cloudflare Workers has long offered first-party Rust/Wasm support, and while Wrangler makes streaming remote logs simple, diagnosing a Rust panic in production has often meant adding more println! statements and redeploying — a slow, brittle loop.

A more systematic approach exists: Wasm core dumps. Though the WebAssembly core dump specification is still a work in progress, the tooling has reached the point where it can be used in production today. This article walks through what Wasm core dumps contain, how to produce them when the runtime doesn't support them natively, and how Cloudflare Workers now integrates the whole flow.

What a core dump captures

A core dump is a snapshot of a program's working memory at the moment of a crash, typically including processor registers and the call stack. On Linux, debuggers like gdb read these files to reconstruct the failure state. Windows has minidumps; interpreted languages like Java and Python have their own equivalents. The common value is post-mortem analysis: determining the cause of a crash after it has happened, from a saved artifact.

In WebAssembly, a crash is called a trap — for example, an out-of-bounds memory access or integer division by zero. Under the experimental core dump spec, trap handling triggers stack unwinding, collecting function parameters, locals, and stack values for each frame, along with binary offsets that map to source locations. The dump also snapshots linear memory, tables, and globals.

To make sense of these binary offsets, the Wasm binary embeds a lighter form of DWARF debugging information. This maps functions and variables to their source names and line numbers. Runtimes like Wasmtime and Wasmer already accept an experimental flag to generate such dumps automatically:

--coredump-on-trap=/path/to/coredump/file

Inspect the resulting file with wasmgdb, the gdb-like debugger designed for Wasm core dumps. For example, a Rust application that deliberately overflows an integer can be diagnosed in a few commands — the backtrace shows each frame with function names, argument values, and source locations, and you can print locals or dereference pointers into memory at known addresses, exactly as you would in gdb on Linux.

Polyfilling core dumps

The Cloudflare Workers runtime, called workerd, does not natively implement the Wasm core dump spec. Since production infrastructure depends on workerd, adopting a standard that is still settling carries risk. But a missing feature does not preclude the technique: the wasm-coredump-rewriter tool rewrites a Wasm binary to inject core dump functionality in userland, similar to how Binaryen's Asyncify instruments binaries for other purposes.

The rewriter replaces each trap instruction (for instance, unreachable) with calls to injected runtime functions that perform the unwinding and capture state before the trap path completes. After the rewrite:

  1. A trapped instruction such as unreachable in addTwo() becomes a call to a $coredump/unreachable_shim function, which records location and debug data and returns normally to the caller.
  2. In the caller, entry(), new code detects that unwinding is in progress, captures the local state, writes the core dump, and then executes the real unreachable trap.

The injected runtime provides these building blocks: $coredump/start_frame(funcidx, local_count) opens a new frame in the dump, $coredump/add_*_local(value) snapshots argument and local values, and $coredump/write_coredump serializes the final file. One implementation detail: the dump is written into the first 1 KiB of the Wasm linear memory, a region neither Emscripten, LLVM's WebAssembly backend, nor most tooling uses. During a crash the host JavaScript catches the exception and can read the dump:

try {
    wasmInstance.exports.someExportedFunction();
} catch(err) {
    const image = new Uint8Array(wasmInstance.exports.memory.buffer);
    writeFile("coredump." + Date.now(), image);
}

The rewriter only intercepts Wasm traps; host function exceptions and memory violations are not caught by default. For cases where regular debugging is enabled, the performance impact is negligible enough that the trade-off is clearly positive, and the instrumentation can be switched off for release builds.

Connecting the pieces in Workers

Cloudflare has been using this polyfill internally to debug Rust-based services like D1, Constellation, and Privacy Edge. To formalize the flow, Cloudflare is now open-sourcing the Wasm Coredump Service, a Worker that collects, parses, and stores core dumps from your applications.

The service is wired to an application Worker through a service binding, which sends HTTP requests between Workers without traversing the public Internet. Setup is brief:

import shim, { getMemory, wasmModule } from "../build/worker/shim.mjs"

const timeoutSecs = 20;

async function fetch(request, env, ctx) {
    try {
        // see https://github.com/rustwasm/wasm-bindgen/issues/2724.
        return await Promise.race([
            shim.fetch(request, env, ctx),
            new Promise((r, e) => setTimeout(() => e("timeout"), timeoutSecs * 1000))
        ]);
    } catch (err) {
      const memory = getMemory();
      const coredumpService = env.COREDUMP_SERVICE;
      await recordCoredump({ memory, wasmModule, request, coredumpService });
      throw err;
    }
}

The shim.mjs import comes from the worker-build tooling, generated automatically when Wrangler compiles a Rust Worker. If the Wasm throws, the handler extracts the core dump from memory and forwards it. The core dump service parses it, prints a stack trace to the logs, and can optionally persist the full dump to an R2 bucket for later inspection in wasmgdb, or forward it to Sentry as an exception.

The code races the shim's fetch() with a timeout because a known wasm-bindgen issue can prevent a Promise from rejecting when Rust panics asynchronously. The timeoutSecs value should be set Just above your app's typical response time.

Dealing with large binaries

Embedding DWARF info plus core dump support can push large projects over the Worker binary size limit. The debuginfo-split tool solves this by extracting DWARF data into a separate debug-{UUID}.wasm file. The UUID is also stored in the original binary, letting the tooling correlate a dump back to its symbols.

command = "... && debuginfo-split ./build/worker/index.wasm"

The stripped binary can be significantly smaller:

4.5 MiB debug-63372dbe-41e6-447d-9c2e-e37b98e4c656.wasm
313 KiB build/worker/index.wasm

Beyond Workers and into the future

A few practical notes. While this article uses Rust, the same rewriter and debugger work for any language that can target Wasm, including C and C++. More importantly, the core dump standard is still evolving. As runtimes like V8 add native support, the polyfill layer may disappear, but for anyone debugging production Wasm on Cloudflare today, the technique is ready now.