There Is No Server
At noon today, Pacific time, I'll open my RubyConf talk on the Main Stage in Las Vegas by asking a few hundred people to take out their phones. The opener doesn't need the room — you can beat them to it:
- Open rubys.github.io/roundhouse/blog — phone is fine.
- Create an article. Anything.
- Open the same URL in a second tab, side by side, and create another. Watch it appear in both.
You just wrote to a database. Everyone reading this post is writing to a database. Whose server is that?
Check the URL bar: github.io. Static file hosting. Nobody's server. You
are running the entire Rails application — router, controllers, Active
Record, SQLite — alone, in your tab. The cross-tab sync is Turbo Streams,
with Action Cable's role played by a
BroadcastChannel out of a SharedWorker. Close the tab and reopen it: your article is still there,
persisted in the browser's origin-private file system. At noon, the room
will do a few hundred Rails deployments in the first ninety seconds of
the talk. You just did yours early.
The talk
The slides are at rubys.github.io/rubyconf-2026, and they ship with the full speaker notes — the complete act-by-act script, not bullet-point shorthand. Press S for speaker view, or open ?script to read the whole talk as a short document. Anyone not in the room can read exactly what the room will hear.
One thing about those slides: the deck is a static page published at the same origin as the demos, so the demos embedded in the slides aren't screenshots or recordings — they're the same live applications you just opened, sharing the same SharedWorker and the same database. The presentation about serverless Rails has no server either. (The demos track the live site and keep improving, so what you see may be slightly ahead of what the room sees.)
The title's debt to The Matrix is deliberate, and it runs deeper than
the title: digital rain falls behind the title slide, and the characters
falling in it are the blog's own Article model. If you're in the room
at noon, there's also a costume — the Keymaker, the man who cuts keys to
every door in a system he doesn't control, which is as good a job
description for a ten-target compiler as I could ask for. Hence the key
at the top of this post.
The argument, compressed: those eight lines of Article model that open
Act III say what, not how. has_many says nothing about query plans;
validates says nothing about the runtime. Rails is to web applications
what SQL is to data — a declarative spec — and once you take that
seriously, the compiler is the query planner and the deployment target is
the execution plan. Your browser is one target. Rust is another. So are
TypeScript, Crystal, Elixir, Go, Python, C#, Kotlin, Swift — and typed,
annotated Ruby that Matz's ahead-of-time compiler,
Spinel, takes to a native binary.
Every commit, CI boots all of them and DOM-diffs every URL against real
Rails; eleven of twelve configurations must come back empty to merge.
This isn't a new idea. It's the first Futamura projection, from 1971:
specialize an interpreter to one program, and the interpreter melts away —
what remains is a compiled program. Rails is the interpreter. Your app is
the program. The weird part was pointing fifty-year-old theory at
has_many.
The talk doesn't argue this so much as demonstrate it, with tooling that
is itself the title restated. The playground is the compiler — roundhouse
compiled to WebAssembly — loaded into the slide, retargeting the blog to
Rust or Elixir or Go as you type. The studio closes the loop from spec to
consequence: edit a validates, and the running app's error message
changes and its test suite goes red, live, in a static page. The IDE is
Monaco over the same wasm analyzer, preloaded with all of Mastodon. The
blog you deployed at the top of this post is a Rails app with no server;
these three are the toolchain with none. Even the query planner runs in
your tab.
See for yourself
The flourish at the center of Act II is one you can reproduce at home,
and it's the strongest receipt that a real Rails app is running
in your tab. You'll want Chrome or Edge for this — chrome://inspect is
Chromium-only.
-
Open the blog and create an article, so the worker is alive.
-
In another tab, go to
chrome://inspect/#workersand click inspect under therubys.github.ioshared worker. A DevTools window opens. -
In the Sources panel, hit Cmd/Ctrl-P and type
articles_controller.rb. That's your first thing to notice: the file tree holds a Rails application — controllers and models underapp/, by their real paths, in Ruby. -
Set a breakpoint in the
createaction, go back to the blog, and submit the New Article form. Execution stops in Ruby — call stack, locals,params, the works. -
Resume, then in the DevTools console:
await new app.Article({title: "hello", body: "from the console"}).save()Every open tab of the blog updates instantly.
The trick has no trick: the compiler emits JavaScript with source maps
that carry the original .rb sources into the bundle, so Chrome debugs
the Ruby you wrote, mapped onto the JavaScript you're actually running.
The numbers
The middle of the talk belongs to a claim I've been building toward all year: Ruby was never the slow part. Same app, same CRuby, same YJIT — melt only the framework, and throughput goes up 8×. At least 87% of a Rails request is the framework re-answering questions whose answers were fixed at boot. YJIT's own verdict points the same direction: +33% on the melted version, −3% on Rails — fresh Arel trees and polymorphic attribute reads every request leave the inline caches nothing to hold. Hand both versions to the JVM and the gap widens to 27×. JITs amplify static shape; Rails erases it. Language accounts for maybe 2–8×; architecture for 8–27×; they multiply.
The ladder isn't an exit from Ruby — it's Ruby's exoneration.
All of these are geomeans across the five endpoints on the project's own live benchmark page, rubys.github.io/roundhouse/bench, republished on a cron from real runs, caveats included. The digits drift as roundhouse develops — the page will have moved by the time you read this — but the conclusions don't hang on the decimals. Don't take the slide's word for it; the raw numbers are a click away.
The robots we live with
The subtitle's third promise is a story with timestamps. On April 30th I
hit a Spinel miscompilation — a module assigned to a module-level
attr_accessor read back as 0. Claude Code analyzed the failure and
drafted the issue; I filed it as
matz/spinel#126 at
02:46:53Z. Matz —
working with his own agent — landed a two-stage fix by 03:39:47Z. I
verified against master and reverted my workaround at 03:58:33Z. Seventy-two
minutes, two humans, two agents, two compilers. Raise your hand if you've
shipped a fix to someone else's compiler in 72 minutes.
The talk presents those 72 minutes as remarkable, and in April they were.
Ten weeks later, that's just the cadence. In the week before this post, I
filed fifteen more spinel issues
— an && evaluation-order violation, a rescue frame popped before its
return expression finished evaluating, String#include? truncating at
NUL bytes — and all fifteen are closed, four of them inside an hour, the
fastest in eighteen minutes. The flow runs both ways now, too:
four pull requests of mine
landed in spinel's codegen the same week, each merged within a couple of
hours. April's story was issue #126; last week's carry numbers in the
2400s.
The receipt I like best from this week is one I have a photograph of.
At 15:38:55Z I filed
matz/spinel#2438 —
group_by silently dropping its nil-key group, no diagnostic, the
rows simply gone at runtime; the exact shape you hit threading comments
by parent_comment_id and reading the roots back out of h[nil].
Nineteen minutes later Matz
closed it,
his agent co-authoring as always. Then I took the photo. In the
foreground, GitHub still calls the commit "3 minutes ago"; in the
background, Matz is on stage, a microphone being clipped to his collar.
He closed my compiler bug and walked on to give his keynote.
The deck's own repository is the other half of that story: every commit
is Co-Authored-By: Claude. The talk about living with the robots was
assembled living with them, and git log is the checkable appendix.
What doesn't work
Candor gets its own act, because the skeptics in the room are owed one.
method_missing never compiles — the answer only exists at runtime.
define_method is a diagnostic today. C extensions are a wall for most
targets (Spinel speaks FFI, so there it's a door). The line isn't the
keyword — it's whether the answer exists before the app runs; has_many
is define_method under the hood, and you watched it compile. Every
unsupported construct becomes a diagnostic on the exact line, and the
diagnostic list is the roadmap, not a gate. Fed all 83,000 lines of
Mastodon, the analyzer types most of it and hands back a ledger of
precisely what it can't yet model — you can browse that ledger yourself,
in an IDE that is also just a tab.
Why any of this matters: Rails gets you to market — everyone agrees on that part. The startups that survive face the sequel: the rewrite, proverbially in Go. Types and performance — the two criticisms WIRED leveled at Ruby this year — are exactly why that rewrite happens. What if the rewrite were a build target? Go, or nine others: readable, idiomatic, yours to evolve independently. Ejection, not lock-in.
Homework
In the room, the ladder slide comes with an apology: every language on it is a live link, and clicking links is not a phone experience. This post is where that homework lands.
- the talk — slides, speaker notes,
git log— rubys.github.io/rubyconf-2026 - the compiler — github.com/rubys/roundhouse
- the numbers, raw — rubys.github.io/roundhouse/bench
- the demos, each running entirely in your tab — the blog, the playground on lobste.rs, the studio, the IDE on Mastodon
- the transpiled apps themselves — each rung downloads as a self-contained tarball of the emitted code, its tests, and a README whose commands CI executes verbatim against the published archive: Rust, Go, Elixir, typed Ruby — the rest are linked from the ladder slide in the deck.
The long form
A thirty-minute talk compresses a year of posts. If you want the full argument behind any act:
- the architecture of the opening demo — the app in a SharedWorker, SQLite in a dedicated Worker with OPFS, presentation on the main thread, under 600 KB gzipped — Three Tiers, Zero Servers
- the ten-target ladder and the oracle that gates it — Spinel on Rails
- why the tenth rung's stack of three compilers is easier than it looks — no compiler in the chain does two jobs — Three Compilers, One Concern Each
- the JVM beat — The Ruby JRuby Was Built to Run
- the week the 72 minutes sits inside — seven issues, six closed, one fix per day — Two Compilers, One Cadence
- whole-app inference, pointed backwards into your editor — Live Types for Rails and Flow Analysis for Rails
- the IDE in a tab — An IDE You Don't Install and Mastodon on Spinel
- what real codebases actually do with the "uncompilable" parts of Ruby — A Bigger Fixture and Dynamic Dispatch
- system tests without the browser tax — Capybara Without the Browser Tax and Full Stack Watch Mode
The close of the talk quotes something I wrote three years ago about JavaScript: the JavaScript you write isn't the JavaScript you run. Ruby has now caught up to its own version of that weirdness. The Ruby you write isn't the Ruby you run. It's Rust. It's your phone. It's Matz's C. Or nothing at all — there is no server.
The last slide of the deck is titled "Continue the argument," and that's the success metric here too. The issues at rubys/roundhouse are open, the speaker notes name every claim's receipt, and the conference recording will be linked here when it's posted.