Today, I'd like to talk about a product called Uzumibi.
I traveled here from Fukuoka, Kyushu—about 1,700 km away, almost the full length of Japan. It's my first time in Hokkaido, and I'm thrilled to speak in this beautiful city.
I'm Kondo, a product engineer at SmartHR—Japan's largest HR SaaS startup and a Platinum Sponsor of RubyKaigi 2026.
Today's theme is an open-source framework called Uzumibi.
It's a framework for developing applications on edge and serverless platforms using Ruby. "Uzumibi" means "buried fire" or embers under ashes, named out of admiration for a certain famous framework.
The key features of Uzumibi are: it has a generator and supports multiple platforms; it uses an easy-to-remember, Sinatra-like DSL; it supports platform integration features like Durable Objects and Queues on Cloudflare;
and above all, it is extremely lightweight.
```ruby res.headers = { "content-type" => "application/json" } res.body = JSON.generate({"message" => "It works!"}) ```
And this isn't just a mockup; It actually connects to real Cloudflare functions, e.g. KVS, Queue... as you write the logics in Ruby.
Uzumibi proves you can comfortably develop edge applications in Ruby. While this example uses Cloudflare Workers, you can write almost the same code and run it the same way on serverless environments like Google Cloud Run. Isn't this incredible?
You might find this strange. ruby.wasm allows you to write applications in Ruby, but it usually generates much larger artifacts—around 10MB. So how is this magic possible?
It's because Uzumibi is based on an entirely different mruby runtime I created, called mruby/edge.
The mruby/edge's motivation was simple: generating small artifacts was too difficult with the CRuby-based ruby.wasm. Before you can understand Uzumibi's power, I need to explain mruby/edge. It's a custom mruby runtime I've been developing since 2014, written entirely in Rust and designed from the ground up to be compiled into WebAssembly.
Because it generates highly portable Wasm, it runs everywhere, including the edge. This is a link to the playground
Lightweight implementations like mruby and mruby/c inherently follow a philosophy of excluding unnecessary code. However, Yukihiro Matsumoto's original mruby relies on C functions like setjmp and longjmp for code jumps.
Since WebAssembly's core instructions lack a goto equivalent, compiling these to Wasm requires hacks. Other implementations like PicoRuby rely on Emscripten for this, meaning the Wasm binary must include Emscripten's bulky runtime, which also forcefully imports and exports several functions behind the scenes.
Given this, I decided I couldn't simply rely on existing Ruby or mruby implementations, and chose to rewrite it from scratch in Rust. Rust offers distinct advantages: productivity and safety via its advanced type system, especially in memory safety, a powerful Wasm ecosystem. Plus, I personally wanted to implement a VM myself with this language.
Note that mruby/edge uses mruby-compiler2 for compiler, which relies on setjmp and longjmp. So, if you include the compiler in your Wasm, it will utilize Emscripten for now. However, if you don't include the compiler, the generated Wasm won't contain any Emscripten codes.
By 2024, a prototype of mruby/edge was complete. Two years ago, I gave a presentation on it at RubyKaigi in Okinawa. However, it was strictly a Proof of Concept,
only capable of running a Fibonacci function for a demo.
At the beginning of 2025, I resumed development. I deeply studied implementations like mruby/c and redesigned the VM.
Once the VM was running, I relentlessly implemented instructions: find a Ruby sample code, confirm it with existing mruby, compile it to bytecode, make it run on mruby/edge, and add it as an E2E test. Through this tedious repetition, mruby/edge gradually matured.
Let me share a few implementation struggles from this period. Here is an mruby instruction to calculate 1 + 2. As you can see, it looks different from CRuby's instructions.
CRuby uses a stack machine, pushing operands onto a stack and consuming them with an add instruction. mruby, however, is a register machine. Operands are stored in registers like R1 and R2, and the add instruction specifies those registers and returns the result to one of them.
A register machine requires specific data structures:
a container for registers, a reference to the instruction sequence called IREP, a program counter, and a so-called callinfo.
IREP contains the members related to the instruction sequence, while CALLINFO holds the call context such as target class.
For memory efficiency and access speed, registers are not hash maps; they are internally implemented as slices. The implementation shifts the start point of the slice every time a function's stack is pushed.
Conceptually, when a function is called, the register offset moves forward, and when it returns, it shifts back.
We also reused Rust's convenient traits wherever possible. A "trait" in Rust is a language feature similar to an interface that defines certain behaviors. For example, a struct implementing the Hash trait can be used as a hash key, and PartialEq allows comparisons.
We map Ruby-level Hashes directly to Rust's standard HashMap. For the key in the Rust HashMap, we insert an enum called ValueHasher, which implements the Hash trait. The actual Ruby objects—key and value—are stored as a tuple on the value side.
Because ValueHasher simply wraps booleans or integers that natively implement the Hash trait in Rust, it acts as a perfect hash key. As a result, internal functions like `mrb_hash_set_index` become incredibly clean and simple.
Next: closures and upvalues. To capture the surrounding environment, we created an Env struct in mruby/edge.
The Env struct has an Option type to hold the parent Env and a vector for captured variables. Interestingly, the array meant to capture the environment doesn't actually copy anything at the moment the lambda is created.
Why? Because a closure's lifetime is usually shorter than the outer method's. As long as the outer method's environment is alive, the internal lambda is fine, so no capture is needed.
But if you return a lambda as a value and use it elsewhere, the method's environment is destroyed, causing a problem.
To solve this, mruby/edge delays the process: it copies the register contents into the capture at the exact moment the frame ends. This avoids unnecessary copies but secures the data when needed. Since each Env holds a reference to its parent, getting an upvalue is just tracing through them.
Let's look at the inheritance tree. In Ruby, singleton classes are highly important.
In Ruby, given class Bar inherits from Foo, `Bar` has its own singleton class.
The inheritance tree of an instance of `Bar` is: Bar's singleton class, then Bar, then Foo, and finally Object, BasicObject.
On the other hand, since Bar itself is a class instance, its inheritance tree is slightly unique:
Bar's singleton class, then Foo's singleton, Object's singleton, BasicObject's singleton, and finally the Class class.
To accurately reproduce this complex chain in Rust, we modified the initialization logic specifically for class instances
...to recursively call the singleton class generation method on the parent class.
Finally, exceptions.
This is a simple Ruby code that raises an exception.
When compiled into mruby bytecode, it's going to look like this.
When execption is raised, then jumps to rescue. The VM extracts the active exception into a register, checks for a match, and either executes the rescue clause or re-raises the error, eventually hitting the ensure block.
When an exception occurs, the VM's state updates to indicate "an exception is active." by assigning the exception to self. While in this state, the VM skips regular instructions and traverses upwards through the blocks until the exception is handled.
By the way, the implementation of break is quite similar. In mruby/edge, break is implemented as a type of exception because it behaves identically, unwinding the call stack and tracing blocks upwards until it finds the invocation point.
This is how the break is represented in the Rust code. It's just another variant of the Error enum.
Ok, let's look at the timeline. After overcoming these struggles, I had a working mruby/edge by mid-2025.
By that time, basic instructions were supported. About 84% of mruby 3.4's instructions were implemented, along with foundational mechanisms to define classes and methods.
For the finishing touch, I needed a standard library. Since the foundation was solid, I had AI write all the basic standard libraries with its E2E testcases.
This experiment was highly successful. Your favorites like String, Array, Hash, and Enumerable were now working.
By early February this year, my custom Ruby was running properly. I originally named the project "mruby/edge" because I intended to run it on WasmEdge. I honestly didn't expect it to run on serverless edge platforms. But since it was running everywhere, I decided to test it on Cloudflare Workers. Since Cloudflare Workers runs JavaScript, it can naturally execute Wasm.
I implemented the necessary functions, built a bridge for the Wasm interface, and aligned the Ruby code. After just a few days of trial and error in December,
I had mruby/edge running on Cloudflare Workers.
Furthermore, when compiled, this mruby code fit into just about 300KB, even uncompressed. Even anticipating a size increase after fully implementing the standard library, I knew it would effortlessly stay under 1MB.
Convinced it was viable, I began developing in earnest. I wrote related libraries and spike codes.
Furthermore, I added support for Fastly Compute, Spin, and Google Cloud Run.
Interestingly, Uzumibi can even run on Web Workers or Service Workers—meaning you can implement an API completely self-contained within the browser.
I also integrated Cloudflare's rich services, like Durable Objects and Queues.
These are introduced as an abstraction layer supporting Cloudflare Workers and Google Cloud Run. For Cloud Run,
similar functions are achieved using Firestore and Cloud Pub/Sub. I'd love to continue adding support for other services, and contributions are highly welcome.
Ok, I'm going to conclude this talk.
You may be surprised by how small the footprints of mruby/edge and Uzumibi are. Why was I able to keep them this simple?
Because This is Simple.
The truth is that the mruby/edge foundation was built so solidly. And I have only implemented a carefully selected set of Ruby methods that are truly essential for most applications. The selection is on COVERAGE.md file in the mrubyedge repository.
In other words, I implemented only the parts of Ruby that I personally wanted to use. Created my own ultimate Pokémon... Sorry, mruby!
When I felt something was not core to mruby/edge, I actively split it into separate crates or excluded it with feature flags. Those units effectively became a gem-like concept. The policy is simple: do not include what an application does not use, and properly implement and include what it does use.
In particular, Rust feature flags are extremely useful because they can fully eliminate unnecessary code from build artifacts. This level of granularity has been very effective for size control.
Matz's mruby describes dependencies with a Ruby DSL. The flow is: download dependencies, compile Ruby to bytecode, and finally link with `ld`. This approach is not bad at all. In this project, however, I leaned toward a Rustacean style.
In Rust projects that include mruby/edge, you can decide which gems to include using Cargo feature flags. On the implementation side, standard macros are enough to specify which code should be included for a gem and which should not. If you write Rust regularly, this API feels extremely rational.
I will quote a line by Hijikata Toshizo, who is closely associated with Hakodate. Though to be fair, it is a fictional line from a novel. 目的は単純であるべきである。 思想は単純であるべきである。 I kept thinking about how to realize simplicity in the simplest possible way.
Based on this simple mruby/edge, Uzumibi just worked without much fuss. I built what I wanted, and eventually created a framework highly useful for everyone.
A key takeaway is that my original approach was right: I wanted a portable Wasm binary with complete control over Wasm-specific features, and I wanted to keep the footprint as small as possible. Also, I wanted to implement it in a straightforward way, following the Rust way.
By achieving this, I believe we are opening up a new horizon on the Edge. Uzumibi is still a newborn framework, but it has the power to transform your development workflows.
I have a little time left, so let's discuss future challenges: asynchronous programming.
Wasm must work seamlessly with JavaScript, where I/O operations—like fetch, or Cloudflare Workers' Durable Objects and Queues—are fundamentally asynchronous. However, Wasm cannot accept asynchronous functions as imports.
As a workaround, a tool called Asyncify emerged, allowing you to pseudo-pass async functions to a Wasm instance. Uzumibi uses Asyncify internally for Cloudflare Workers support.
But a massive downside is binary bloat—it increases size by about 1.5 times. I started this project to fix bloated binaries, so relying on Asyncify is unacceptable.
The same issue applies to native servers, used in Cloud Run framework.
Rust server libraries such as Hyper, Tokio require async implementation for performance. But the current implementation of mruby/edge is totally synchronous.
For this restriction, I/O operations are performed on a single thread. You might think single-threaded I/O would be a bottleneck. But on Cloud Run, you can spin up many single-threaded containers instead of using multi-core within a single process. It works, but it's a serverless-specific workaround—not truly general-purpose. So I want to make mruby/edge itself async-compatible.
Looking at the current VM - it runs a straightforward synchronous instruction loop—unchanged since 2024.
But notice that the VM is essentially a state machine.
If we design it to pause and resume at arbitrary points, it should integrate well with async programming.
Let me demonstrate an async VM PoC running in the browser. Just like two years ago, I'll execute a Fibonacci function.
First, let's compute Fibonacci all at once.
In this case, the browser gets no control back during the computation—the UI completely freezes.
Next, let's compute Fibonacci while yielding control back to the browser after each instruction. This time the UI stays responsive.
Since the instruction loop runs on the browser side, we can insert async function calls with some glue code.
Here's one caveat: the browser loop maxes out at around 250 instructions per second, so yielding on every single instruction is too fine-grained. The right batch size needs further investigation. I wish I could show a fully production-ready async mruby here, but that remains homework for the future.
Today I introduced mruby/edge and the Uzumibi framework.
While hurdles remain, it already has quality sufficient for practical use. From an exclusively Ruby-centric world, it's often hard to step into serverless and edge computing.
But with mruby/edge, you can develop with high compatibility using the language you love.
Please give it a try—I look forward to your feedback.
Thank you very much!