CPU Limits¶
The runtime does not own a global CPU quota. Hosts should enforce time budgets around execution and treat cancellation as part of the embedding policy.
Wasmtime execution is a native call boundary. Future.get(timeout, unit) limits
how long the host waits, but it does not prove that the Wasm engine stopped at
that exact instant. Keep untrusted execution on a dedicated worker or process so
timeouts, cancellation, and process isolation are controlled by the embedder.
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
val executor = Executors.newSingleThreadExecutor()
val future = executor.submit<LongArray> {
instance.export("run").apply()
}
try {
val result = future.get(500, TimeUnit.MILLISECONDS)
println(result.contentToString())
} catch (_: TimeoutException) {
future.cancel(true) // sets Thread.interrupt() on the worker thread
throw IllegalStateException("Wasm execution timed out")
} finally {
executor.shutdownNow()
}
For untrusted code, keep all blocking host functions under the same timeout policy. A timeout cannot preempt arbitrary host code that ignores cancellation.
Use WasmtimeExecutionConfig for limits the Wasmtime store can enforce during
instantiation and execution. maxFuel enables Wasmtime fuel metering for guest
Wasm instructions:
val instance =
Instance.builder(module)
.withExecutionBackend(ExecutionBackend.PULLEY)
.withWasmtimeExecutionConfig(
WasmtimeExecutionConfig(
maxMemoryBytes = 64L * 1024L * 1024L,
maxWasmStackBytes = 256L * 1024L,
maxInstances = 1,
maxTables = 32,
maxMemories = 4,
maxFuel = 5_000_000,
),
)
.build()
Fuel is a store budget, not a wall-clock timer. A module can be instantiated and
wait for the host to call an export without consuming fuel. Fuel is consumed only
while guest Wasm instructions run; if the budget is exhausted, Wasmtime traps the
call. WasmtimeUnlimitedResourceLimit (-1) disables fuel, 0 is allowed and
traps as soon as guest execution needs fuel, and positive values are the
available budget.
For a regular Instance, the budget belongs to that Wasmtime store and is shared
across exported calls on the same instance. For WasmtimePreview3ComponentConfig,
the Preview 3 bridge creates a fresh store per component call or command run, so
maxFuel is effectively a per-call budget. Precompiled Preview 3 components
must be compiled with fuel enabled, for example wasmtime compile -W fuel=1 ...,
otherwise Wasmtime rejects deserialization under a fuel-enabled engine.
Fuel does not measure host work. It does not tick while the guest is idle, while a
host import is blocked in I/O, or while the application waits outside Wasmtime.
Keep blocking imports under the same timeout and cancellation policy as the Wasm
entry call. On wasmJs, KRWA uses the browser or Node WebAssembly engine, which
does not expose Wasmtime fuel; use workers/timeouts or guest instrumentation for
that target.
Coroutines and WASI Preview 3¶
Coroutine cancellation is not a CPU quota. withTimeout, Job.cancel(), or
closing a WASI Preview 3 host scope can cancel suspending host work, but they do
not preempt CPU-bound Wasm execution unless the Wasm call or resume is running
under the runtime interruption policy above.
WASI Preview 3 host tasks and coroutine-scheduled P3 resumes run in a host-owned
child job of the configured CoroutineScope or CoroutineDispatcher. Closing
the host cancels that child work without cancelling a caller-supplied scope. A
dispatcher with parallelism greater than one can use multiple CPU cores, so
billing based only on elapsed wall time may under-count work. If one-core
accounting is required, use withResourceBudget(parallelism = 1) so dispatcher
parallelism and the P3 async resource limits are set together.
withResourceBudget(...) is not a CPU meter. Its parallelism value is the
maximum amount of P3 work KRWA may run at the same time, not a measurement of
how much CPU the guest actually consumed. For example, a host may allow
parallelism = 6 while a particular guest only keeps two cores busy. The
resource budget still caps exposure at six concurrent lanes, but it does not
prove that six CPU cores were used.
Use one of these accounting models:
- For simple one-core accounting, set
parallelism = 1and charge elapsed time. - For conservative multi-core accounting without CPU metering, charge at most
elapsedWallTime * parallelism. - For actual CPU accounting on the JVM, run each tenant or request on a
dedicated worker pool and sum the pool thread CPU time with platform APIs
such as
ThreadMXBean.getThreadCpuTime(...). - For stricter production accounting, isolate execution in a process or container and use OS or cgroup CPU counters.
Coroutines and dispatchers do not provide portable per-coroutine CPU usage. They are scheduling and cancellation primitives; they cannot tell the host that one coroutine used two CPU-seconds while another used six. If exact CPU metering matters, keep the metered work isolated from unrelated host work.
On non-JVM targets there is no host Thread.interrupt() equivalent. Use
target-specific cancellation around the host call boundary, and prefer small,
bounded host functions for untrusted workloads.