In September 2026, Rails World came to Austin, its first edition in the United States. As you’d expect, almost every talk found its way to AI. But what stuck with me was seeing Rails show up in places it didn’t use to go.
I’ve worked with Ruby on Rails for years, and for most of that time the conversation has been the same. Rails is great for getting started, but when the topic turns to performance or a small binary you can ship, the answer is usually: another language.
Except I’d been following something that would change that conversation. In April 2026, at RubyKaigi, Matz introduced Spinel, a Ruby compiler. Spinel reads your whole program, works out the type of every variable, parameter, and return value, and generates C, which the system’s C compiler turns into a binary. What you get is a native executable that runs on a machine without Ruby.
To see why that’s possible, and why it isn’t trivial, it helps to understand what changes when Ruby is compiled: the questions it normally answers while the program runs have to be answered before it starts.
Changing tires on a moving car
Take this method:
def total(price, quantity)
price * quantity
end
puts total(10, 3)
When Ruby reaches price * quantity, it still doesn’t know what price is. It could be an Integer, a Float, a BigDecimal, or one of your own objects that defines *. So on every call, it checks what it got before deciding how to do the math, and on the next call, it checks again. That’s what makes Ruby so flexible, and also what makes it slower than it could be. YJIT helps, but the analysis still happens while the program runs.
Spinel answers up front
./spinel total.rb
./total
At compile time, Spinel looks at every call to total in the program. If they all pass integers, it already knows price and quantity are always integers, and price * quantity becomes an integer multiplication in C. Nothing needs checking at run time. The payoff shows up in the benchmarks in the README: on average, Spinel comes out about 8 times faster than Ruby 4 with YJIT.
But settling everything up front has a cost: anything that can only be resolved while the program runs is out. For example:
evalon a string;method_missing;Class.newat run time;define_methodwith a name built on the fly;class Stack < Array.
The included and inherited hooks can be defined, but they never fire. include is resolved at compile time, so there’s no moment, with the program running, when it “happens”.
Does that mean Spinel rejects all metaprogramming? No. Things like send(:save) work, so does define_method(:name) with a literal, and instance_eval with a block compiles. One question separates what gets in from what stays out:
Can you tell what this line will do just by reading the code?
When you can’t, Spinel warns or refuses at compile time. This program, for instance, runs on any Ruby:
class Stack < Array
def peek = last
end
s = Stack.new
s.push(1)
p s.peek
Spinel won’t build the binary, and it tells you why:
spinel: class Stack < Array: subclassing Array is not supported yet (a subclass
would answer differently from CRuby); wrap an Array in an instance variable instead
It isn’t a vague error. It says what’s missing, why, and what to do instead.
In principle, this won’t change: Spinel will remain a subset of Ruby, because CRuby is the oracle. Every test in the project is a .rb program with its expected output next to it, generated by running the program on CRuby. If the Spinel binary prints something else, the bug is Spinel’s.
What about Rails?
If you’ve ever been curious enough to open up Rails and see how it works inside, you may have spotted the problem. Rails is full of metaprogramming, and much of it only resolves at run time:
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true
end
Active Record only discovers the columns by querying the database, and only then creates the methods. has_many, for example, defines methods when the class is loaded. That’s exactly the kind of thing Spinel leaves out. And yet, here’s the wild part: Campfire, 37signals’ chat app, already runs compiled by Spinel!
You might be wondering how, after everything I just said about Spinel. The answer is that the code you write isn’t the code Spinel compiles. Before it, another tool, Sam Ruby’s Roundhouse, does the heavy lifting: it reads the Rails app and swaps the questions Rails only answers at run time for code that already has the answers. So that same Article comes out the other side looking roughly like this:
class Article < ApplicationRecord
attr_accessor :id, :title, :body, :created_at, :updated_at
def comments
@_comments ||= ArticleCommentsProxy.new(self)
end
def validate
validates_presence_of(:title)
end
end
The columns come from the migrations, has_many becomes a plain method, and validates becomes an explicit call. There’s no question left to answer while the program runs. This rewrite has its own tricks and limits. Roundhouse isn’t perfect, and Spinel only compiles part of Ruby. But together, they take the conversation from the beginning of this post somewhere new.
With that in mind, should we write code thinking about how it will be rewritten, or how it will be interpreted? What matters more, flexibility or speed? And what if we could have both, with a little extra work? In part, that’s what we already have with Rails, and it’s what Spinel and Roundhouse are trying to push further.