The WebAssembly decision record: why the execution model you chose determines your sandbox boundary, your performance ceiling, and your plugin ecosystem lock-in surface

Published 2026-07-12 · WhyChose

WebAssembly decisions are made in a single architecture session, usually when a team identifies a specific requirement that WebAssembly appears to solve cleanly: sandboxed execution of third-party plugin code, compute-at-the-edge without cold-start container overhead, or a cross-language extension point that accepts code from plugin authors who write in different languages. The AI session that produces the Wasm decision is thorough by the standards of that moment. It compares Wasm against the main alternatives — native shared libraries, subprocess isolation, V8 Isolates, Linux containers — and concludes that Wasm offers a useful combination of properties: near-native performance with ahead-of-time compilation, memory isolation without a process boundary, and a binary format that any language with a wasm32 target can emit. The decision is documented in a comment in the CI configuration, or as a note in the architecture document that someone started writing and never finished, or not at all — existing only in the AI session log from the day the team decided to use Wasm.

What the AI session does not produce is the decision record's second half. The session asks "should we use WebAssembly?" and answers yes. It does not ask: what is the specific threat we are sandboxing against, and does the Wasm memory model plus the WASI capability provisioning model actually address that threat? Which languages will plugin authors use, and which of those languages have production-ready wasm32-wasi toolchains at the WASI version we are targeting? What WASI version are we targeting, and what is the migration plan when the component model's interface definitions change? What is our cold-start latency budget, and have we profiled whether module instantiation overhead dominates that budget under production workload conditions? What resource limits — instruction count, memory size, wall-clock time — are required to prevent a misbehaving module from affecting other tenants, and are those limits enforced by default in the runtime we chose, or do they require explicit configuration? Each of these questions has an answer that is not derivable from the decision to use WebAssembly. Each answer constrains the plugin system design, the security posture, the language support matrix, and the performance model in ways that compound over the two or three years before the consequences become visible. The answers exist in the AI session. They are the reasoning behind "yes, Wasm." They are almost never written down.

Two ways WebAssembly decisions produce the wrong outcome

The plugin system interface story

A developer productivity tool company — a code review and documentation assistant for engineering teams — decides to build a plugin system that lets enterprise customers extend the product with custom logic: custom code smell detectors, custom documentation templates, custom review workflow hooks. The founding team evaluates the plugin architecture in a two-hour AI session. The alternatives considered are: native shared libraries (fast, unsafe — a misbehaving plugin can segfault the host process), subprocess isolation via stdin/stdout IPC (safe, but high overhead — a new process per invocation, hundreds of milliseconds per call), V8 Isolates (fast sandboxed execution, but requires plugin authors to write JavaScript or TypeScript — the target customer base is polyglot), and WebAssembly. The session concludes in favor of Wasm: memory isolation without process overhead, cross-language support via the wasm32-wasi target, and near-native execution speed. The team publishes a plugin SDK that wraps the wasi_snapshot_preview1 interface with a higher-level Rust API, ships two example plugins (one in Rust, one in TinyGo), and opens the plugin system to enterprise customers.

Twenty-six months later, the plugin system has fourteen enterprise customers with active plugins, across a mix of languages: eight in Rust, three in TinyGo, two in C++, and one in AssemblyScript. The Rust plugin authors are the most productive and have the most complex plugins. The team publishes a new version of the host application that upgrades the Wasm runtime from wasmtime 12 to wasmtime 19. The upgrade is necessary for a performance improvement in the AOT compilation pipeline and for WASI preview 2 support, which the product team plans to use to provide a richer capability interface. The upgrade breaks all fourteen enterprise plugins.

The breakage has three independent causes. First, the wasmtime 12-to-19 upgrade changes the ABI of the host functions that the plugins import — not the WASI interface itself, but the padding and alignment conventions for memory layouts in the host shim layer that the SDK generated. Every plugin must be recompiled against the new SDK, not just re-linked. Second, the team updated the WIT interface definitions for the component model as part of the WASI preview 2 migration. Between WASI preview 2 RC0 (which the new SDK targets) and the RC2 that shipped with wasmtime 19, three interface definitions changed: the wasi-filesystem interface's descriptor type dropped a field, the wasi-clocks interface's subscribe-duration function's signature changed, and the wasi-http interface's incoming-response type's status field moved from a u16 to a named enum. The generated language bindings change accordingly. Plugins using the filesystem, clock, or HTTP interfaces — seven of the fourteen — cannot be recompiled against the new SDK without source changes. Third, and most consequentially: the team discovers that two of the fourteen plugin authors no longer work at the customer companies that commissioned the plugins. The plugins exist as compiled .wasm binaries, but the source code is either lost or held in a personal repository that the plugin author took with them when they left. Those two plugins cannot be recompiled against any version of the SDK. They stop working on the new host version and cannot be recovered without a rewrite from scratch.

None of these outcomes were unforeseeable. The WASI specification changelog was public. The wasmtime ABI stability policy — which explicitly does not guarantee compatibility across minor versions for the SDK's shim layer — was documented in the wasmtime repository's RELEASES.md. The risk of compiled plugins losing their source code is a known risk for any plugin system that distributes binary artifacts without requiring source escrow. What the founding AI session did not produce was a plugin API stability commitment: a documented policy stating which interface elements are stable across host application versions, what the upgrade notification period is, and what migration support the team provides when a breaking change is necessary. Without that commitment, the fourteen enterprise customers had no basis for planning their plugin maintenance cycle. Without a source escrow requirement in the plugin SDK's onboarding documentation, two plugins became irretrievably lost when their authors left. The decisions were all made implicitly, in the gap between "let's use WebAssembly" and the operational policies required to ship a production plugin system on top of it. The new CTO who inherits this system will find fourteen plugins, seven broken, two source-lost, and no document explaining the stability commitment the company made or failed to make to plugin authors.

The user-defined function execution story

A data transformation startup builds a multi-tenant platform where customers submit custom data transformation logic as code — functions that accept structured records and emit transformed records — and the platform executes those functions at scale against the customer's data streams. The technical challenge is multi-tenant sandboxed execution: customer A's transformation code must not be able to read customer B's data, write to customer B's output, or consume enough CPU or memory to degrade customer B's performance. The founding team evaluates the execution model in an AI session. The alternatives are: Python subprocesses with subprocess32 isolation (safe boundary, but fork overhead is 50-100ms per invocation — unacceptable at the volume they expect), Google V8 Isolates via the isolated-vm npm package (fast, good isolation, but requires transformation code in JavaScript or WebAssembly — the target customer base writes Python and Rust), Linux containers via gVisor (good isolation, reasonable startup, but 200-500ms per cold container creation — also unacceptable at the expected call volume), and WebAssembly via wasmtime with WASI preview 1. The session concludes: Wasm provides per-module memory isolation (each customer's function gets its own linear memory address space), accepts code compiled from any language with a wasm32-wasi target, and wasmtime's pre-compiled AOT cache means a warm instantiation is 0.5-2ms. The team builds the execution harness, integrates wasmtime, and ships.

Fourteen months after launch, a security audit commissioned for an enterprise procurement review finds a critical issue: the execution harness does not enable wasmtime's fuel metering API. A customer transformation function that contains an infinite loop — intentional or accidental — consumes 100% of one CPU core for the duration of the wasmtime execution timeout (which is not configured and defaults to none). A single such function, called repeatedly from the customer's data stream ingestion, can starve all other customer functions sharing the same host process, because the fuel metering check that would terminate the loop after a configurable instruction budget is not present in the execution path. The harness does enforce a wall-clock timeout via a background thread that kills the wasmtime store after a fixed duration, but this timeout is five seconds — enough for legitimate long-running transformations — and the CPU starvation during that five-second window affects all tenants sharing the process. The platform has been running in this configuration for fourteen months across an expanding customer base. The security audit cannot determine whether the vulnerability was exploited; the Caddy access logs are rotated weekly and the oldest available logs are seven days old.

Separately, the team discovers that the largest enterprise customer's engineering team writes transformations in Go. Go compiles to wasm32-wasip1 via GOOS=wasip1 GOARCH=wasm since Go 1.21, but the GC proposal — the WebAssembly garbage collection extension that enables native-GC languages to compile to smaller, more efficient Wasm modules — requires a runtime that supports the wasm-gc proposal. Wasmtime 14 added wasm-gc support. The team is running wasmtime 12. The customer's transformations compile and run, but the GC proposal is not active, which means the compiled binaries use a software GC implementation embedded in the Wasm module binary, which increases the per-function binary size from the expected 500KB to 4.2MB and introduces unpredictable GC pauses inside the execution boundary. The customer's p99 transformation latency is 340ms against a contracted SLA of 50ms. The fix requires upgrading the wasmtime runtime to version 14, which requires retesting the entire customer transformation corpus against the new runtime, which requires the platform team to build a test harness they do not currently have, which takes three weeks of engineering time on the critical path to SLA compliance. The original AI session chose wasmtime but did not specify a runtime version pin, a runtime upgrade policy, or a test harness requirement for validating the transformation corpus against runtime upgrades. Those decisions were implicitly deferred to "when we need to upgrade," which turned out to be immediately — at the first enterprise customer with non-trivial Go code — and the deferred cost was a three-week engineering sprint and a month of SLA breach.

Three structural properties that WebAssembly decisions determine

The sandbox security boundary and the capability provisioning model

WebAssembly's sandbox provides two properties by design, and does not provide a third property that teams commonly assume is included. The two guaranteed properties are memory isolation and import-controlled host access. Memory isolation: each Wasm module instance has its own linear memory — a contiguous byte array that starts at zero — and cannot address memory outside that range without an explicit host-exported memory-sharing function. A module cannot read or write the host process's stack, its own .text segment, or another module instance's linear memory through any Wasm instruction. This isolation is enforced by the Wasm binary format validator — a module that references addresses outside its linear memory boundary fails validation before it executes. Import-controlled host access: a Wasm module can only call functions that the host explicitly makes available in its import set. A module that has not been granted the WASI fd_open function cannot open files, regardless of what its code attempts. A module that has not been granted the sock_accept function cannot make or accept network connections. This makes the host's capability provisioning policy the primary control surface for the sandbox: the security of the execution environment is determined by which host functions the module is permitted to import, not by the module's code itself.

The property that WebAssembly does not provide by default is protection against algorithmic complexity attacks. A module that implements an infinite loop — a while(true) { } that never modifies external state — cannot be detected by the memory isolation mechanism (no cross-module memory access occurs) or the import control mechanism (no restricted imports are called). It runs, consuming CPU time proportional to the host's throughput, until either the host terminates the execution explicitly or the host process is killed by an external supervisor. Wasmtime's fuel metering API addresses this by instrumenting the module's execution with instruction counting and a configurable budget; when the budget is exhausted, the runtime raises a fuel exhaustion trap that the host can catch. But fuel metering is not enabled by default. Configuring it requires: calling store.add_fuel(budget) before each invocation, setting an appropriate budget (calibrated to the p99 legitimate execution cost plus a safety margin), handling the OutOfFuel error in the invocation error path, and logging fuel exhaustion events for monitoring. Wasm's memory limit (configured via the module's memory section or at instantiation via the MemoryType limits argument) controls the maximum linear memory size; without this, a module can call memory.grow repeatedly until the host process exhausts its virtual memory budget. Both limits — fuel and memory — must be explicitly configured and tested; neither is present by default. The security threat model must specify whether the execution environment needs to protect against algorithmic complexity attacks, and if so, the WebAssembly decision record must enumerate the specific runtime mechanisms used to enforce the limits, the configuration values chosen, and the test suite that verifies the behavior under an intentional infinite loop and under a legitimate but long-running computation.

The WASI capability provisioning policy is the second axis of the sandbox security model. WASI preview 1 exposes capabilities as a flat set of imported functions; the host grants capabilities by including or excluding those functions from the module's import set. The minimum capability set for a transformation function that reads input records from an in-memory buffer and writes output records to an in-memory buffer is: no WASI capabilities at all — the host passes data through Wasm memory, not through WASI file descriptors. Any WASI capability beyond this minimum — fd_read, fd_write, path_open, sock_connect, environ_get, clock_time_get — expands the module's access surface and must be justified by a documented requirement. The capability provisioning policy is not a one-time decision; it must be maintained as plugin authors request additional capabilities, as new WASI capabilities are standardized, and as the component model's interface definitions stabilize. The most common mistake is granting the full WASI preview 1 capability set to all modules because it is the path of least resistance in the wasmtime WasiCtx builder — WasiCtxBuilder::new().inherit_stdin().inherit_stdout().inherit_stderr().inherit_env().inherit_args() grants the majority of WASI preview 1 capabilities to the module, including filesystem access to the host's current directory and full environment variable access. That is rarely the intended sandbox boundary.

The performance ceiling and the execution model selection

WebAssembly's performance relative to native code depends on three factors: the compilation mode (AOT ahead-of-time versus JIT), the workload profile (CPU-bound versus I/O-bound, long-running versus short-duration), and the language of origin (Rust/C/C++ compile to tight Wasm bytecode; languages with a GC runtime compile to larger, less predictable binaries). AOT-compiled Wasm, compiled from Rust or C with wasmtime's cranelift or llvm backend, runs within ten to twenty percent of equivalent native code for CPU-bound workloads with simple memory access patterns. The performance gap comes from Wasm's linear memory model — all heap allocations are offsets into the linear memory array rather than native pointer dereferences, which adds one level of indirection relative to native code and prevents some compiler optimizations — and from indirect call overhead through the Wasm function table. For the class of workloads that motivated most Wasm adoption decisions — data serialization and deserialization, pattern matching, structured record transformation — this gap is acceptable and often irrelevant relative to the I/O overhead of the surrounding system.

The performance dimension that is most commonly underestimated is cold-start overhead: the time between "the host decides to invoke this module" and "the first Wasm instruction executes." Module instantiation in wasmtime — creating a new Store, configuring WASI context, instantiating the pre-compiled module, setting up the linear memory — takes approximately 0.5ms to 3ms for a typical plugin-sized module (100KB to 1MB compiled .wasm). That cost is paid on every module instantiation. For execution patterns where each invocation creates a new module instance — the serverless-style "one isolation context per request" model — instantiation time is on the critical path for every request. For a service with a 10ms p99 latency target and a 1ms module instantiation overhead, Wasm adds ten percent overhead in the best case; at 2ms instantiation, it adds twenty percent. The mitigation is module instance pooling: keep a warm pool of pre-instantiated module instances per module and return them to the pool after invocation. Pooling reduces instantiation overhead from the per-request critical path to an amortized background cost, but it requires each pooled instance to be safely reusable across invocations — which means the module's linear memory must be reset to its initial state between invocations, global variables must be re-initialized, and any mutable state imported from the host must be re-bound. Wasmtime's InstancePre API enables efficient pool-based instantiation, but using it correctly requires understanding which parts of the module state are per-invocation and which can be shared across the pool lifetime. These are implementation details that are not derivable from "we use WebAssembly." They are design decisions that belong in the WebAssembly decision record under the performance model section, with the expected per-request instantiation overhead, the pooling strategy, and the benchmark methodology used to validate that the performance model is correct under production-realistic load patterns.

The language-of-origin factor affects both binary size and runtime predictability. Rust, C, and C++ produce compact Wasm binaries with predictable allocation behavior. TinyGo and AssemblyScript produce compact binaries with a simple bump allocator; garbage collection pauses are minimal because the GC is simple. Standard Go (via GOOS=wasip1) and languages that compile their runtime to Wasm — Python via Pyodide or MicroPython, Ruby via ruby.wasm — produce binaries that are 3MB to 30MB, with GC pause behavior inherited from the runtime's GC implementation. For plugin systems where the host controls the compilation step, language support can be scoped to the languages that meet the binary size and runtime predictability requirements. For plugin systems where external authors compile their own plugins, the language support matrix is determined by the WASI capability set and the component model's toolchain support for each language — and the decision record must document which languages are officially supported, what binary size limits are enforced (and how), and how unsupported languages are handled at the submission boundary.

The component model evolution and the plugin ecosystem lock-in surface

The WebAssembly component model — specified as WASI preview 2 and standardized through the Bytecode Alliance's WASI standardization process — is the long-term architecture for composable Wasm modules with typed interface definitions. It provides capabilities that WASI preview 1 does not: per-component capability grants at the interface level (a component can import only the specific WASI interfaces it declares in its WIT definition), interface versioning (WIT definitions are versioned and the component model supports interface adapters between versions), and first-class interface composition (components can import and export typed interfaces from other components, not just host functions). The component model is the correct long-term architecture for plugin systems and composable Wasm platforms. It is also a breaking change from WASI preview 1 in its binary format, toolchain requirements, and interface definitions.

The lock-in surface of a WebAssembly plugin system is defined by the combination of the WASI version target, the WIT interface definitions published for the plugin API, and the Wasm runtime selected. A plugin system that publishes WASI preview 1 bindings has a lock-in surface bounded by the preview 1 import set. Migration to the component model requires publishing new WIT interface definitions, providing new SDK versions that target the component model, supporting a migration period where both preview 1 and component model plugins run in the same host, and communicating a deprecation timeline for preview 1 support. Each of these steps is a coordination cost paid by the plugin system operators against the plugin author ecosystem. The cost is proportional to the number of active plugins and the diversity of the plugin author base. A plugin system with three internal plugin authors can migrate in a single sprint. A plugin system with fourteen enterprise customers, each with plugins maintained by different engineering teams, requires a migration plan that spans multiple quarters and includes backward compatibility commitments. The ADR lifecycle decision record applies here: the original WebAssembly execution model ADR should be superseded when the WASI version target changes, with a new ADR that documents the migration rationale, the interface changes, the backward compatibility policy for existing plugins, and the timeline for deprecating preview 1 support. Without that supersession record, the history of why the interface changed — and why certain backward compatibility trade-offs were made — exists only in the Slack threads and AI sessions from the migration planning period.

The API gateway decision record is adjacent to the WebAssembly execution model decision for teams that use Wasm at the traffic layer. Envoy's WASM filter API and Kong's WASM plugin model both embed a Wasm runtime in the request-processing path, which means the gateway's Wasm runtime version, the ABI stability of the filter interface, and the capability provisioning model for gateway plugins are determined by the gateway runtime, not by a separate Wasm execution harness the team builds. The WebAssembly decision record must note whether Wasm is used in the application layer (host-controlled runtime), the gateway layer (runtime-embedded, ABI determined by the gateway), or both, and whether the two layers use the same or different WASI version targets — because a team that upgrades the application layer's Wasm runtime independently of the gateway's embedded runtime may encounter different behavior between the two layers for modules that were expected to behave identically.

Three AI session types that embed WebAssembly decisions without documenting them

The plugin architecture session is where the execution model is chosen and the capability surface is implicitly set. The team identifies the plugin requirement, evaluates alternatives, and concludes that Wasm is the right tool. The session produces a clear answer to "should we use Wasm?" and produces the initial plugin SDK design. What the session does not produce is the plugin API stability commitment: which interface elements are stable across host versions, what the upgrade notification period is, what the migration support policy is, and whether plugin authors are required to maintain source availability for their compiled modules. These commitments are not part of "the plugin architecture" in the way teams usually frame the question; they are operational policies that are determined after the architecture is chosen. But they should be documented in the same decision record, because they are constraints on the architecture's long-term viability. A plugin system without a stability commitment is not a product feature; it is a prototype that happens to have enterprise customers. The platform engineering decision record for an internal developer platform that uses a Wasm plugin model faces the same problem at a smaller scale: the platform's Wasm execution model determines which teams can write plugins, in what languages, with what interface stability guarantees — and those decisions determine whether the platform will be adopted or worked around.

The multi-tenant execution model session is where the threat model is implicitly set and the resource limits are not set. The team identifies the multi-tenant isolation requirement, evaluates subprocess isolation versus containers versus Wasm, and concludes that Wasm's per-module memory isolation plus low instantiation overhead is the right fit. The session produces a clear answer to "which isolation mechanism?" and produces the execution harness architecture. What the session does not produce is the resource limit enforcement specification: the fuel budget calibration methodology, the memory limit configuration, the wall-clock timeout value and its relationship to the fuel budget, and the test suite that verifies the termination behavior under intentional resource exhaustion. These specifications are derived from the threat model — and the threat model is the precise statement of what the isolation mechanism is protecting against. Without a documented threat model, the resource limit configuration is guesswork, and the gap between the assumed threat surface and the actual sandbox boundary is discovered through a security audit rather than through a deliberate design decision. The open-source extractor is built to surface exactly these multi-tenant execution model sessions from AI chat history, because the founding session contains the threat analysis — the AI session's reasoning about why subprocess isolation was too slow, why containers were too heavyweight, why Wasm's memory isolation was the right level of isolation — and that reasoning is the context required to evaluate whether the resource limit configuration actually addresses the threat.

The edge compute performance session is where the WASI version target is implicitly locked and the cold-start constraint is assumed rather than measured. The team is evaluating whether to run a latency-sensitive workload at the edge using Wasm modules served by a Wasm-capable CDN or edge runtime (Cloudflare Workers, Fastly Compute, Deno Deploy). The AI session evaluates the edge runtime's performance characteristics — cold start, memory limits, execution timeout — and concludes that Wasm at the edge is viable for the target latency budget. What the session does not produce is the WASI version pin: which WASI version does the edge runtime support, and what is the plan if the edge runtime upgrades its embedded Wasm runtime to a newer WASI version that is not backward-compatible? Edge runtimes control their own Wasm runtime upgrade schedule. A team that publishes modules targeting WASI preview 1 on a runtime that subsequently migrates to the component model may find their modules require recompilation on a timeline driven by the edge runtime's upgrade schedule, not their own. The decisions never written down post identifies this class of implicit decisions — the decisions that were correct at the time they were made, are technically locked by the environment rather than the team's choice, and become visible as a migration requirement only when the environment changes. The edge runtime's WASI version target is exactly this type of decision: invisible until a version change makes the existing module binaries non-functional.

The five sections of a WebAssembly decision record

The first section documents the execution model selection and use case rationale. This section answers the question that the founding AI session answered — why WebAssembly over the alternatives — but structures the answer in a form that is evaluatable by a future engineer who was not in the session. The use case must be specific: "plugin system where external authors submit compiled modules that extend the product's core processing pipeline" is evaluatable; "we need sandboxed execution" is not. The alternatives comparison must include the specific reasons each alternative was rejected: subprocess isolation was rejected because fork overhead of 50-100ms per invocation exceeded the latency budget; V8 Isolates were rejected because the target plugin author population includes Rust and C++ engineers who should not be required to wrap their code in JavaScript; Linux containers were rejected because the expected per-invocation volume of 10,000/minute made 200ms cold-start container creation unacceptable even with pre-warmed pools. The evaluation criteria that drove the selection — isolation latency, language support breadth, binary size limits, runtime embedding model — should be listed explicitly, so that a future engineer evaluating whether the Wasm decision is still correct can apply the same criteria to the current context rather than reconstructing them from the code that was built on the assumption that those criteria were correct.

The second section documents the sandbox security model, threat level, and capability provisioning policy. The threat model must be stated at the level of the actual attacker: is the execution environment protecting against code from external, potentially hostile authors (highest threat — requires fuel metering, memory limits, capability minimization, and adversarial test suite); from customers who are trusted business partners but whose code is not reviewed before execution (medium threat — requires resource limits and capability minimization, but adversarial scenarios are less likely); or from internal engineers who are deploying their own code (low threat — resource limits for availability protection, but adversarial scenarios are not the primary concern). The capability provisioning policy must enumerate which WASI capabilities are granted to each module type and the rationale for each grant. For WASI preview 1, this is the set of imported functions available in the WasiCtx. For the component model, this is the set of WASI interface imports declared in the module's WIT world definition. The section must also document the resource limit configuration: fuel budget per invocation, memory limit per module instance, wall-clock timeout, and the test suite that verifies the limits are enforced (including a test that verifies an infinite-loop module is terminated within the configured fuel budget without affecting other modules in the same process). The security ADR is the upstream document that defines the threat level; the WebAssembly decision record's capability provisioning section is the downstream document that translates the threat level into specific runtime configurations.

The third section documents the runtime selection and WASI version target. The Wasm runtime is a significant architectural commitment: wasmtime (Bytecode Alliance, Rust, AOT and JIT), WASMer (Rust and C, multi-backend), wasm3 (C, tree-walking interpreter, no JIT — suitable for microcontrollers and embedded contexts), WASM Time for .NET (Microsoft's wasmtime bindings), and the V8 engine (Google, JavaScript and Wasm, used by Node.js, Deno, and all Chromium-based browsers). Each has a different ABI for host functions, a different fuel metering API, different pre-compilation and caching mechanisms, and a different roadmap for WASI and component model support. Switching runtimes after the execution harness is built requires rewriting the host function shim layer, re-profiling the performance characteristics, and validating the resource limit behavior — a significant engineering effort. The runtime selection rationale should document the decision criteria (embedding model, language, WASI support, AOT compilation support, ecosystem maturity) and the current version pinned, with a note on the runtime's ABI stability policy. The WASI version target must be explicit: WASI preview 1 (wasi_snapshot_preview1), WASI preview 2 component model (the stable release), or a preview-1-compatible component model adapter. The section must also document the component model migration plan: when will the plugin system migrate from preview 1 to the component model, what will the migration process be for existing plugins, and what backward compatibility guarantees are provided during the migration period. For teams that are not yet ready to commit to a migration timeline, the WASI version pin and a stated "migration deferred" with a review trigger is sufficient — the goal is that the decision is documented rather than implicit, so that the migration question is revisited deliberately rather than forced by a runtime upgrade.

The fourth section documents the language support matrix and component model adoption plan. The language support matrix enumerates which source languages are officially supported for plugin development, with the specific toolchain version and WASI target required for each. For WASI preview 1: Rust (stable toolchain, wasm32-wasi target), C/C++ (wasi-sdk 20+, --sysroot flag required), TinyGo (0.31+, -target wasi flag), AssemblyScript (0.27+, --target wasi), and Go (1.21+, GOOS=wasip1 GOARCH=wasm, with the note that GC pauses may affect predictable-latency use cases). The component model adoption plan per language: Rust has stable component model support via the cargo-component toolchain and wit-bindgen; TinyGo has experimental component model support as of 0.32; C/C++ via wasi-sdk supports component model output with --target wasm32-wasip2; Go component model support is in progress; Python via componentize-py provides component model output for specific WASI world definitions. The section should document the binary size limit enforced at module submission time — this is the operational control that prevents Python or Ruby runtime-embedded modules from being submitted to a plugin system designed for compact compiled modules. A binary size limit of 2MB rejects CPython-embedded modules (15-30MB) while accepting all Rust, C, TinyGo, and AssemblyScript modules. The limit should be documented with the rationale (memory overhead per instantiation, pool size implications, module load time) rather than stated as a bare number. The container orchestration decision record is the relevant comparison document for teams evaluating whether the language support constraint of a Wasm plugin system is a feature or a limitation: containers impose no language constraint but impose a higher startup overhead; Wasm imposes a language constraint but offers near-native startup for the supported languages.

The fifth section documents the performance budget, resource limits, and cold-start constraint. The performance budget must be stated in terms of the actual workload: what is the p50 and p99 execution time for a representative set of production module invocations, what is the acceptable module instantiation overhead as a fraction of total request latency, and what is the maximum acceptable memory footprint per module instance pool? These numbers are not derivable from "we use WebAssembly." They require profiling the actual workload — or a representative synthetic workload — against the chosen runtime, compilation mode, and module pool configuration. The cold-start constraint must document whether module instance pooling is required (yes for latency-sensitive workloads; optional for batch workloads), the pool sizing methodology (minimum pool size, maximum pool size, eviction policy for modules not invoked within a configured window), and the linear memory reset strategy for pooled instances (memset to zero, custom reset function, or module re-instantiation). The resource limits section documents the fuel budget value and calibration methodology, the memory limit value and the WASM memory type configuration used to enforce it, the wall-clock timeout value, and the monitoring that detects when a module is terminated by resource limits versus completing normally. Resource limit termination events should produce observable signals — log entries, metrics — that are distinct from normal completion, because an increasing rate of fuel exhaustion terminations is a signal that either the legitimate workload is growing beyond the configured budget or that adversarial inputs are targeting the resource limits deliberately.

The plugin architecture session, the multi-tenant execution model session, and the edge compute performance session each produce WebAssembly decisions whose long-term cost — a fourteen-month fuel-metering exposure in a multi-tenant platform, or a WASI preview 2 migration that breaks all fourteen enterprise plugins simultaneously — exceeds what a structured decision record would have cost at the time the decision was made. The decisions are in the AI chat history: the threat model reasoning that compared Wasm against subprocess isolation and containers, the WASI version selected for the initial SDK, the language support matrix that the founding team assumed would cover the plugin author population. WhyChose's open-source extractor surfaces these sessions as structured records before the next runtime upgrade, plugin API change, or security audit makes the undocumented assumptions visible as failures. The new CTO onboarding problem is also a Wasm problem: an incoming technical leader who inherits a plugin system or a multi-tenant execution platform will find compiled .wasm binaries, a wasmtime version pin, and no document explaining which WASI capabilities are granted to each module, what resource limits are enforced, or what the plugin API stability commitment is — because the decisions that produced those answers exist only in the AI sessions from the day the founding team chose WebAssembly.

Further reading