---
title: "Kobako: Resource Limits"
date: 2026-09-09T00:00:00+08:00
publishDate: 2026-09-09T00:00:00+08:00
lastmod: 2026-09-07T21:25:44+08:00
tags: ["LLM","AI","Experience","Ruby","WebAssembly","Gem","mruby"]
series: "kobako"
toc: true
aiTranslated: true
permalink: "https://blog.aotoki.me/en/posts/2026/09/09/kobako-resource-limits/"
language: "en"
---


In [Kobako: Exchanging Memory in WebAssembly](https://blog.aotoki.me/en/posts/2026/08/19/kobako-webassembly-memory-exchange/) we solved the problem of Ruby and WebAssembly interacting, and gave Ruby and mruby the ability to exchange values with each other, which basically cleared up the "sandbox" problem.

Kobako's development is still at a very early stage, though, because there are plenty of problems that only show up in real use, and one of them is the problem of "deciding how to stop."

<!--more-->

## The Ideal Case{#ideal-scenario}

Under normal circumstances we expect users to know exactly what they're doing, so writing a piece of code that works properly feels like a given, and this is the kind of implementation we'd naturally expect to see.

```ruby
sandbox = Kobako::Sandbox.new
sandbox.eval("1 + 2").value  # => 3
```

Reality isn't like that, though. What if a user accidentally writes an infinite loop?

```ruby
sandbox = Kobako::Sandbox.new
sandbox.eval("loop { puts 1 }").value  # => ?
```

This is reasonable in both Ruby and mruby. In embedded systems, mruby's main battleground, it's very common to need to run some feature or service this way, so we can't forbid users from running code like this. But if we don't stop the user, this sandbox will keep on existing until the host runs out of resources.

## Execution Limits{#runtime-limit}

[Wasmtime](https://wasmtime.dev/), which Kobako uses, comes with a Fuel mechanism that can put a very explicit limit on the resources used, but in actual testing Fuel made Kobako run slower, so we switched to Epoch to limit execution time instead.

The reason Fuel is slower than Epoch is that it has to check every single WebAssembly instruction, whereas Epoch simply specifies a time, so it's quite a bit faster overall. Here's roughly how it's done in Kobako.

First, when creating Wasmtime, we enable Epoch and spawn a lightweight thread that increments the Epoch every 10 ms.

```rust
let mut config = Config::new();
config.epoch_interruption(true);
let engine = Engine::new(&config)?;
{
    let engine = engine.clone();
    thread::spawn(move || loop {
        thread::sleep(Duration::from_millis(10));
        engine.increment_epoch();
    });
}
```

Every time we use `Sandbox#eval` or `Sandbox#run`, a brand new mruby sandbox is started, and at the moment it starts we set the point where it should stop based on the `timeout` setting.

```rust
let deadline = Instant::now() + Duration::from_secs(1);
store.set_epoch_deadline(1);

store.epoch_deadline_callback(move |_ctx| {
    if Instant::now() >= deadline {
        Err(anyhow!("wall-clock deadline exceeded"))
    } else {
        Ok(UpdateDeadline::Continue(1))
    }
});
```

Whenever the Epoch updates, we check whether we've gone past the allowed point in time, and if so we throw an error right away to interrupt the sandbox currently executing, which makes sure no infinitely executing state can happen (unless `timeout: nil` is set deliberately, which lets it through)

This way we can control execution time at a relatively low cost, and we don't have to worry about a badly written implementation, or an implementation generated incorrectly by an LLM (Large Language Model), hogging resources.

## Memory Limits{#memory-limit}

Now that execution time gets cut off, memory has the same problem, such as loading a huge amount of data by mistake, or causing trouble some other way.

```ruby
sandbox = Kobako::Sandbox.new
sandbox.eval("'S' * 2**1024").value  # => ?
```

Wasmtime itself has a `ResourceLimiter` trait available, where every time more memory needs to be allocated it asks first, and can only be used once permitted, so Kobako added a mechanism like this.

```rust
struct MemoryLimiter { limit: usize, baseline: usize, active: bool }

impl ResourceLimiter for MemoryLimiter {
    fn memory_growing(&mut self, _cur: usize, desired: usize, _max: Option<usize>)
        -> Result<bool>
    {
        if !self.active { return Ok(true); }
        if desired - self.baseline > self.limit {
            bail!("memory usage exceeded memory_limit");
        }
        Ok(true)
    }
    fn table_growing(&mut self, ..) -> Result<bool> { Ok(true) }
}


store.limiter(|state| &mut state.limiter);

state.limiter.baseline = memory.data_size(&store);
state.limiter.active = true;
let result = export.call(&mut store, params);
state.limiter.active = false;
```

Because mruby itself also takes up memory, if running mruby needs `1 MB` of memory on its own, going straight by "how much memory was added" would eat up a `1 MB` cap instantly, which is why the `baseline` and `active` design exists.

Once initialization is done, we first check how much memory mruby has used, record it as `baseline`, then mark `active` as enabled and let it carry on executing. The `target memory - baseline` we record at that point is the memory the script actually takes up, so we can make sure a call gets the full memory allowance.

Although neither execution time nor memory is a "precise calculation" but rather a rough estimate, even this much is enough to stop mistaken or malicious use, making sure the host isn't affected by an abnormal implementation inside the sandbox, and mitigating to some degree the damage of executing unsafe Ruby scripts.

Of course, this also guards against attacks like ReDoS (Regular Expression Denial of Service) to some extent, but there are still many kinds of attack techniques, and limiting resources is only one of them. To make execution safer we still have many other kinds of trade-offs, which will be added to Kobako one after another.

