matthew-a-thomas opened issue #13467:
Hello,
What is the recommended way to limit the rate at which guest code executes?
It looks like fuel consumption is a good fit for either measuring the execution cost of guest code, or for forcing unexpectedly expensive guest code to trap. But neither of these are the same as rate limiting guest code. And unfortunately it doesn't seem straightforward to modify the fuel value once a mutable reference to the
Storehas been thrown over the wall to aFunc::call_async(...).And it looks like epochs are a good fit for ensuring some measure of fairness between guests on the host. But this also is not the same as rate limiting guest code.
This is my rate limiting code so far. It feels very hacky because it relies on the epoch deadline _always_ being expired. But this is the only way I've found that lets me inspect and top up the fuel once the guest function has started, and it does seem to work. Any thoughts on a cleaner approach?
use wasmtime::{Config, Engine, Linker, Module, Store, UpdateDeadline}; #[tokio::main] async fn main() -> anyhow::Result<()> { let performance = Performance { fuel_per_second: 10000f64, jerks_per_second: 50f64, }; let mut config = Config::default(); config.consume_fuel(true).epoch_interruption(true); let engine = Engine::new(&config)?; let linker = Linker::new(&engine); let module = Module::new( &engine, r#" (module (func (export "main") (loop $loop br $loop ) ) ) "#, )?; let mut store = Store::new( &engine, State { performance: performance.clone(), }, ); const FUEL: u64 = u64::MAX; store.set_fuel(FUEL)?; store.fuel_async_yield_interval(Some(performance.fuel_per_jerk()))?; store.epoch_deadline_callback(|mut store| { let consumed_fuel = FUEL - store.get_fuel()?; if consumed_fuel == 0 { return Ok(UpdateDeadline::Yield(0)); } store.set_fuel(FUEL)?; let delay_secs = consumed_fuel as f64 / store.data().performance.fuel_per_second; let delay = tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay_secs)); Ok(UpdateDeadline::YieldCustom( 0, Box::pin(async move { println!("{consumed_fuel}:{delay_secs}"); delay.await; }), )) }); let instance = linker.instantiate_async(&mut store, &module).await?; let main = instance.get_typed_func::<(), ()>(&mut store, "main")?; main.call_async(&mut store, ()).await?; Ok(()) } #[derive(Clone, Debug)] struct Performance { pub fuel_per_second: f64, pub jerks_per_second: f64, } impl Performance { pub const fn fuel_per_jerk(&self) -> u64 { f64::max(1.0f64, self.fuel_per_second / self.jerks_per_second) as u64 } } struct State { performance: Performance, }
cfallin commented on issue #13467:
Perhaps you could use either epochs or fuel to ensure regular interruptions (
Poll::Pendingfrom the underlying async call into Wasm), then build a Future combinator around that inner future to do what you want?I could imagine e.g. a Future combinator that tracks wallclock time used by each
poll(); and then explicitly returnsPendingwith a later scheduled wakeup (or an await on a timer or whatever) if a guest's "quantum" (1ms per second or whatever) has already been used.
cfallin commented on issue #13467:
In other words, don't use fuel or epochs directly as the unit; use wallclock time as the unit; and use the underlying fuel or epochs solely as a mechanism to grab control back at regular intervals, and choose to yield if time is up.
fitzgen commented on issue #13467:
And unfortunately it doesn't seem straightforward to modify the fuel value once a mutable reference to the
Storehas been thrown over the wall to aFunc::call_async(...).The idea is that you give as much fuel as you are okay executing all at once, without interruption, then when execution runs out of fuel, you check your rate limits and either
- wait for some amount of time/delay and repeat the process,
- kill the Wasm instance and store, or
- resume with some more fuel and repeat the process.
(Or do similar with epochs instead of fuel.)
matthew-a-thomas commented on issue #13467:
if a guest's "quantum" (1ms per second or whatever) has already been used.
choose to yield if time is up.
The difficulty I'm having is that I want the quantum to be in units of fuel, not time.
I hope I'm understanding you properly, but if I use a future combinator, then I do not see how I can also have host functions implemented asynchronously. I say this because the
FutureI get fromcall_asyncappears to yield when a host function (of course called by the guest function) yields. Therefore I cannot tell if the yield is fromStore.fuel_async_yield_interval, for example. I think this means I cannot tell how much fuel has actually been consumed.The idea is that you give as much fuel as you are okay executing all at once, without interruption, then when execution runs out of fuel, you check your rate limits and either
- wait for some amount of time/delay and repeat the process,
- kill the Wasm instance and store, or
- resume with some more fuel and repeat the process.
(Or do similar with epochs instead of fuel.)
I'm not sure what "repeat the process" would mean in the general case where there will be observable side effects. I'm hoping to implement throttling in a way that is invisible to client code.
I suppose I _could_ require clients to implement something akin to Arduino's
loopfunction (which my host would invoke at a throttled rate), rather than amainfunction (which my host would call once and let run continuously at a throttled speed). But I was hoping for themainfunction approach because it leads to more natural client code.Thank you.
matthew-a-thomas edited a comment on issue #13467:
if a guest's "quantum" (1ms per second or whatever) has already been used.
choose to yield if time is up.
The difficulty I'm having is that I want the quantum to be in units of fuel, not time.
I hope I'm understanding you properly, but if I use a future combinator, then I do not see how I can also have host functions implemented asynchronously. I say this because the
FutureI get fromcall_asyncappears to yield when a host function (of course called by the guest function) yields. Therefore I cannot tell if the yield is fromStore.fuel_async_yield_interval, for example. I think this means I cannot tell how much fuel has actually been consumed.The idea is that you give as much fuel as you are okay executing all at once, without interruption, then when execution runs out of fuel, you check your rate limits and either
- wait for some amount of time/delay and repeat the process,
- kill the Wasm instance and store, or
- resume with some more fuel and repeat the process.
(Or do similar with epochs instead of fuel.)
I'm not sure what "repeat the process" would mean in the general case where there will be observable side effects. I'm hoping to implement throttling in a way that is invisible to client code.
I suppose I _could_ require clients to implement something akin to Arduino's
loopfunction (which my host would invoke at a throttled rate), rather than amainfunction (which my host would call once and let run continuously at a throttled speed). Theloopapproach would let me set fuel to the highest tolerable value, callloop, inspect the consumed fuel, delay accordingly, and repeat. But I was hoping for themainfunction approach: it leads to more natural client code; and I'd have to think how to define "the highest tolerable value".Thank you.
matthew-a-thomas commented on issue #13467:
Another thought which crossed my mind is to preprocess the client WASM myself. I could instrument the instructions and insert calls to a delaying host function. This has the advantage of generalizing well across all WASM runtimes but it would add complexity.
cfallin commented on issue #13467:
The difficulty I'm having is that I want the quantum to be in units of fuel, not time.
That's fine, but "limit... rate" requires a unit that is a rate, right? Fuel is a unit of computation, not a unit of computation rate. So to limit execution rate, you'll need to limit fuel per time.
(Consider what a limit of just "N units of fuel" means: that means you get N ops... forever. "N units of fuel per second" is a rate that describes how fast the guest progresses.)
The reason I suggested "time per time" (1ms per second or whatever) is that it is a unitless (dimensionless) fraction that can be reasoned about in a principled way in a multitasking system: all such rates can add to at most 1 (minus some switching and system overhead). Because fuel is tied to Wasm opcodes, which can have wildly different costs (both due to implementation strategies and due to dynamic effects like cache misses or branch mispredictions), it's hard to reason about "fuel per second" and how to apportion it in a way that leads to a stable system. Curious to hear how you were thinking about this, though.
matthew-a-thomas commented on issue #13467:
@cfallin Thank you for your reply.
you'll need to limit fuel per time
Yes, this is my goal. I would like to throttle the rate at which guest _code_ (the computation itself) executes.
My motivation is just a fun personal project. I'd like to make an emulator for a fictional processor that exists only in my head. This processor has a limited rate of computation per time, and I'd like the emulator to behave very similarly across a wide variety of hosts. I'm only trying to learn something new and to keep my Rust skills from ...ahem ..._rusting_. WebAssembly is appealing to me because among the things that are at least a little bit like processor instruction sets, it seems to virtualize very well and it is a well-supported Rust target so I can use Rust on both the host and on the client, and there seems to be very good host tooling (such as
wasmtime).how to apportion it in a way that leads to a stable system
I am planning to start with quite low computation rates (in the order of only thousands of opcodes per second). If all the abstractions feel good then I might turn this fictional processor into a plot element in a multiplayer game or something. Whether that will amount to anything I do not know, but for now it pulls me more in the direction of determinism (as in a third party being able to validate the outcome of a player's emulated processor) than performance (as in cramming as many emulators onto a host as stability will allow).
Thank you again.
Last updated: Jun 01 2026 at 09:49 UTC