Stream: git-wasmtime

Topic: wasmtime / issue #14313 Proposal: first-class Instance::r...


view this post on Zulip Wasmtime GitHub notifications bot (Sep 10 2026 at 20:10):

stevendore opened issue #14313:

Feature

We embed wasmtime in a high-throughput HTTP proxy (the embedding from #14312) and pool WASI 0.2 component instances across requests, resetting each instance to pristine state between checkouts. Today we do this from the embedder side via two small introspection accessors we carry as a local patch. That patch is ~90 additive lines with tests. We are working through our company's open source approval process to be able to offer it upstream, and will link the PR here once we can. Either way it is the minimal primitive, not the complete solution: this issue proposes the runtime-owned reset that the accessors cannot deliver.

The feature: a way to reset a live instance back to its just-instantiated state, for example component::Instance::reset(&self, store) -> Result<()>, and possibly a core Instance::reset as well. Reset rewinds linear memories, tables, mutable globals, and internal flags (data/elem drops, lazy funcref init) so the instance is indistinguishable from a fresh instantiation of the same InstancePre in the same store, without re-running start or component initializers.

Benefit

It makes instance reuse safe for untrusted guests, which is currently a gap. wasmtime serve reuses instances by trusting the guest to keep itself request-ready (#9542), default-on for WASIp3 but deliberately default-off for WASIp2 (max_instance_reuse_count defaults to 1). Embedders whose p2 guests are untrusted or operator-supplied cannot opt into trust-based reuse at all, and per-request instantiation is a host-wide kernel ceiling, not a per-core cost.

The three strategies below all give every request a pristine instance. They differ in who restores the state and whether the kernel is involved:

Measured on a dual-socket Xeon Gold 6330 (2x28 cores, 112 threads with SMT), dispatch-bound component call, aggregate calls/s:

threads fresh instantiate (on-demand) fresh instantiate (pooling allocator) reset-based reuse (embedder-side)
1 26.0K 36.7K 143.9K
8 26.3K 92.8K 971.3K
96 21.3K 191.1K 4.32M

On-demand instantiation is flat from 1 to 96 threads, with per-call latency inflating from 0.026 ms to 2.88 ms as threads queue on the kernel. The pooling allocator is better but stalls near 191K/s: only 2x more throughput for 12x more threads past 8. The userspace reset scales with cores: 22.6x the best instantiate-per-request strategy at 96 threads. Per-call isolation overhead at 1 thread: 25.2 us (on-demand) and 17.1 us (pooling) vs 5.8 us (reset).

A runtime-owned reset would beat our embedder-side numbers further and close a correctness gap at the same time:

Implementation

Semantics: rewind to the post-instantiate point, as above. The runtime already holds everything needed: the memory image as the pristine reference, initial global values and element segments from the module, and the internal flags.

Two mechanism notes from our production experience that a design should account for:

There are also aspects of the runtime we are not familiar enough with to know how they would interact with a reset: fuel and epochs, async and concurrent state, resource handles, resetting while other instances share the store, and whether the pooling allocator could service a reset without unsharing images. We defer to maintainers on all of those. Happy to share the full benchmark methodology and an embedder's-eye API review now, and to contribute the accessors patch and implementation work under maintainer direction once our approval process completes.

Alternatives

view this post on Zulip Wasmtime GitHub notifications bot (Sep 10 2026 at 20:43):

pchickey commented on issue #14313:

This reads to me as LLM-generated english to the point where I can't be confident that you (the human) and I share any understanding via this artifact, so I'd like to direct you to https://github.com/bytecodealliance/governance/blob/main/AI_TOOL_POLICY.md, once you have made any edits required to comply with that policy you can respond and I'll try to re-read.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 10 2026 at 22:14):

alexcrichton commented on issue #14313:

Benchmarking-wise for something like this it's expected to basically ignore the deafult instance allocator as that's known to have poor performance in this scenario. The pooling allocator, however, is intended to be tailor-made for this sort of scenario, and that's the benchmark we typically use.

Otherwise the numbers you've measured here look a bit suspect to me -- low hundreds-of-thousands is roughly what I'd expect for a setup like this, but low-millions is quite surprising to achieve. That'd be great if we could get close to that number, but I'd want to dig into what you're doing. It sounds like you're more-or-less doing a memset of linear memories to reset them, and I'd expect any non-negligible-sized linear memory to tank performance with such a memset. I realize you're describing a compare-and-only-write-if-different strategy which might help perf a bit, but I wouldn't expect that to be an order of magnitude faster than a memset.

Put another way -- in addition to turning down the LLM output a bit it'd be helpful to be able to poke around what you're comparing. Whether or not this is a true reset of instance state matters for correctness and is something at least I'd want to review.

Orthogonally I might also recommend investigating tuning pooling allocator options:

view this post on Zulip Wasmtime GitHub notifications bot (Sep 15 2026 at 02:40):

stevendore commented on issue #14313:

Thank you for the tuning suggestions, they helped in all cases. linear_memory_keep_resident and pagemap_scan(Auto) were the most helpful, and those two are what the tuned pooling arm below uses.

Overall I work with pretty small wasm guests (~137 KB .wasm, 256 KiB linear memory, only a few functions each) and found that only resetting the memory space is more efficient for my use case of needing a fresh image on every request. I put the whole comparison in a gist, every permutation we discussed plus the reset with and without the tuning:

https://gist.github.com/stevendore/52c9458e09c5342e93f61bfbe9f7787f

Two files: the accessors patch and a single test file that drops into crates/wasmtime/tests/ of a patched checkout. The test file has a correctness test that runs in the normal suite and the benchmark behind #[ignore]. Run steps are in the file header, with MEM_PAGES / THREADS / SECS / ROUNDS / DIRTY knobs to reproduce everything below. The benchmark guest is a minimal embedded component whose linear memory defaults to 256 KiB, matching my production guests, and MEM_PAGES scales it.

The permutations, all giving each call a pristine instance except the floor:

The table numbers are from a 2x28-core Xeon (112 threads with SMT), kernel 6.9.6 (pagemap active), DIRTY=2, 3 rounds x 5s per arm.

256 KiB guest (my case), total calls per second across all threads:

THREADS fresh on-demand fresh pooling fresh pooling tuned reset-reuse reset on tuned pooling no-reset floor
1 52.8K 126.6K 201.6K 218.4K 217.0K 1.96M
8 49.4K 399.0K 885.4K 1.76M 1.76M 15.6M
32 41.5K 901.7K 1.92M 5.88M 5.99M 52.6M
96 35.3K 1.04M 2.54M 10.7M 10.8M 89.5M

The reset is a full memcmp of the linear memory per call, so its cost grows with guest size while the pooling arms stay flat. Crossover was around 0.5 MiB in my tests.

4 MiB guest (MEM_PAGES=64), total calls per second across all threads, shows the existing pooling allocator is more performant:

THREADS fresh on-demand fresh pooling fresh pooling tuned reset-reuse reset on tuned pooling no-reset floor
8 50.6K 403.1K 766.1K 129.8K 129.5K 15.7M
32 43.7K 884.8K 2.08M 414.9K 426.0K 52.0M

Tuned pooling wins by 5-6x at this size. The two reset columns are within a few percent of each other everywhere, the reset does not touch the allocator, so the tuning only shows up when instances get created (pool fill, periodic recycle).

view this post on Zulip Wasmtime GitHub notifications bot (Sep 15 2026 at 17:54):

alexcrichton commented on issue #14313:

One thing I notice in your code is that you're not using InstancePre for repeated instantiation, so I'd definitely recommend updating that.

Otherwise though could you perhaps perform analysis to determine why your strategy is faster than the "fresh pooling tuned" column? For example are syscalls the bottleneck? Memory traffic? etc.

One other possible dimension is that you can forcibly disable PAGEMAP_SCAN and then also set keep-resident values high. That in theory should be a head-to-head comparison of your memory reset logic with this reset logic. It should in theory be possible to tweak the built-in logic to basically be the same as your logic so I would expect that in the limit these should basically perform the same.


Last updated: Sep 20 2026 at 18:08 UTC