Three properties of the WASM plugin runtime that cost something on every request today, and that also gate any future heavyweight plugin. Surfaced while assessing WAF integration options in ADR-0031, where they were prerequisites for the sidecar and TinyGo routes. The route that ADR chose is a native pipeline stage, so none of this blocks the WAF work. Filing it separately so it can be scheduled on its own merits.
They are independent of each other and can land in any order.
P1. A fresh WASM instance is created and init()-ed on every request, per middleware
InstancePool::get_instance builds a new PluginInstance and calls init(config) each time it is invoked:
crates/barbacane-wasm/src/pool.rs:223 (get_instance)
crates/barbacane-wasm/src/pool.rs:260 (instance.init(&config_json))
and it is invoked per request, per middleware:
crates/barbacane/src/main.rs:1576 (middleware chain construction)
crates/barbacane/src/main.rs:1866 (dispatch)
Modules are AOT-compiled and cached, so this is instantiation plus guest-side init, not compilation. For the current plugin set that is cheap but not free: a request through five middlewares pays five instantiations and five init calls, and init runs guest code that parses the plugin's config JSON.
The name InstancePool suggests instances are pooled. They are not; the type caches modules and configs.
What to do: reuse instances keyed on (plugin, config), which is what InstanceKey already models, so init runs once per key rather than once per request. Needs a decision on guest state: a reused instance carries whatever the guest left in its linear memory, so either instances are reset between requests or plugins gain a documented contract that on_request must not depend on prior state. That contract question is the real work here, not the pooling.
Unmeasured. I have not benchmarked this, so I am not claiming a number. A benchmark comparing the current path against a reused instance across the existing plugin set would size it, and is worth doing before the change rather than after.
P2. Plugin resource limits are engine-global, so every plugin gets the largest plugin's ceiling
PluginLimits defaults to 16 MB of linear memory, 100 ms wall clock and 100 M fuel (crates/barbacane-wasm/src/limits.rs:28), and the data plane installs one set for the whole engine:
crates/barbacane/src/main.rs:734
The floor is then scaled by max_body_size, so a deployment that accepts large uploads raises the ceiling for all 33 plugins at once, including the ones that only read a header.
What to do: per-plugin [limits] in plugin.toml, carried through PluginCapabilities into the artifact manifest and enforced per instance. This is also what would let a genuinely memory-hungry plugin exist without handing the same budget to everything else.
P3. The WASI surface is seven hand-written stubs, and a missing import fails at instantiation
crates/barbacane-wasm/src/instance.rs:2213-2371 defines stubs for random_get, clock_time_get, fd_write, sched_yield, environ_get, environ_sizes_get and proc_exit. Nothing calls Linker::define_unknown_imports_as_traps, so a plugin importing anything else fails to instantiate, with an error that points at the missing symbol rather than at the cause.
A wasm32-wasip1 guest from most toolchains also imports args_get, args_sizes_get, fd_close, fd_fdstat_get, fd_seek and poll_oneoff.
wasmtime-wasi is already declared as a workspace dependency (Cargo.toml:105) and is used nowhere: the only reference in the tree is the comment at instance.rs:2217 saying it could replace the stubs.
What to do: either adopt wasmtime-wasi with a capability-gated view, or keep the stubs and call Linker::define_unknown_imports_as_traps(&module) (plural, taking the module; verified against wasmtime 47.0.3). That gives every unresolved import a trapping stub, so instantiation succeeds and the trap fires only if the guest actually calls one. The second is smaller and arguably better for a sandbox: it keeps the import surface explicit, denies by default, and turns "this plugin will not load" into "this plugin trapped on a call it should not have made", which is easier to diagnose.
Corrected: an earlier version of this issue named the method in the singular, which does not exist.
Why this is worth doing independently of the WAF
P1 is the only one with a per-request cost today, and it applies to every plugin in every deployment. P2 and P3 are latent: they cost nothing until someone writes a plugin that needs more memory than its neighbours, or one built with a toolchain whose WASI imports differ, at which point they present as confusing failures rather than as limits.
None of the three is on the critical path for ADR-0031, which compiles rules at build time and evaluates them in a native pipeline stage next to the existing per-operation validators (crates/barbacane/src/main.rs:654-655).
Refs: ADR-0031, section "Runtime prerequisites (P1-P3)".
Three properties of the WASM plugin runtime that cost something on every request today, and that also gate any future heavyweight plugin. Surfaced while assessing WAF integration options in ADR-0031, where they were prerequisites for the sidecar and TinyGo routes. The route that ADR chose is a native pipeline stage, so none of this blocks the WAF work. Filing it separately so it can be scheduled on its own merits.
They are independent of each other and can land in any order.
P1. A fresh WASM instance is created and
init()-ed on every request, per middlewareInstancePool::get_instancebuilds a newPluginInstanceand callsinit(config)each time it is invoked:crates/barbacane-wasm/src/pool.rs:223(get_instance)crates/barbacane-wasm/src/pool.rs:260(instance.init(&config_json))and it is invoked per request, per middleware:
crates/barbacane/src/main.rs:1576(middleware chain construction)crates/barbacane/src/main.rs:1866(dispatch)Modules are AOT-compiled and cached, so this is instantiation plus guest-side
init, not compilation. For the current plugin set that is cheap but not free: a request through five middlewares pays five instantiations and fiveinitcalls, andinitruns guest code that parses the plugin's config JSON.The name
InstancePoolsuggests instances are pooled. They are not; the type caches modules and configs.What to do: reuse instances keyed on
(plugin, config), which is whatInstanceKeyalready models, soinitruns once per key rather than once per request. Needs a decision on guest state: a reused instance carries whatever the guest left in its linear memory, so either instances are reset between requests or plugins gain a documented contract thaton_requestmust not depend on prior state. That contract question is the real work here, not the pooling.Unmeasured. I have not benchmarked this, so I am not claiming a number. A benchmark comparing the current path against a reused instance across the existing plugin set would size it, and is worth doing before the change rather than after.
P2. Plugin resource limits are engine-global, so every plugin gets the largest plugin's ceiling
PluginLimitsdefaults to 16 MB of linear memory, 100 ms wall clock and 100 M fuel (crates/barbacane-wasm/src/limits.rs:28), and the data plane installs one set for the whole engine:crates/barbacane/src/main.rs:734The floor is then scaled by
max_body_size, so a deployment that accepts large uploads raises the ceiling for all 33 plugins at once, including the ones that only read a header.What to do: per-plugin
[limits]inplugin.toml, carried throughPluginCapabilitiesinto the artifact manifest and enforced per instance. This is also what would let a genuinely memory-hungry plugin exist without handing the same budget to everything else.P3. The WASI surface is seven hand-written stubs, and a missing import fails at instantiation
crates/barbacane-wasm/src/instance.rs:2213-2371defines stubs forrandom_get,clock_time_get,fd_write,sched_yield,environ_get,environ_sizes_getandproc_exit. Nothing callsLinker::define_unknown_imports_as_traps, so a plugin importing anything else fails to instantiate, with an error that points at the missing symbol rather than at the cause.A
wasm32-wasip1guest from most toolchains also importsargs_get,args_sizes_get,fd_close,fd_fdstat_get,fd_seekandpoll_oneoff.wasmtime-wasiis already declared as a workspace dependency (Cargo.toml:105) and is used nowhere: the only reference in the tree is the comment atinstance.rs:2217saying it could replace the stubs.What to do: either adopt
wasmtime-wasiwith a capability-gated view, or keep the stubs and callLinker::define_unknown_imports_as_traps(&module)(plural, taking the module; verified against wasmtime 47.0.3). That gives every unresolved import a trapping stub, so instantiation succeeds and the trap fires only if the guest actually calls one. The second is smaller and arguably better for a sandbox: it keeps the import surface explicit, denies by default, and turns "this plugin will not load" into "this plugin trapped on a call it should not have made", which is easier to diagnose.Corrected: an earlier version of this issue named the method in the singular, which does not exist.
Why this is worth doing independently of the WAF
P1 is the only one with a per-request cost today, and it applies to every plugin in every deployment. P2 and P3 are latent: they cost nothing until someone writes a plugin that needs more memory than its neighbours, or one built with a toolchain whose WASI imports differ, at which point they present as confusing failures rather than as limits.
None of the three is on the critical path for ADR-0031, which compiles rules at build time and evaluates them in a native pipeline stage next to the existing per-operation validators (
crates/barbacane/src/main.rs:654-655).Refs: ADR-0031, section "Runtime prerequisites (P1-P3)".