Ruby has the better compilation story now. Not “is catching up.” Not “could rival.” It has lapped Python on every axis I care about as someone who runs both in production. When Spinel hit Hacker News this morning at 309 points, the comments split predictably into two camps: Rubyists who already knew, and Pythonistas who insist Mojo will eventually fix everything. I have spent the last four years shipping Ruby and Python to production at scale — Sidekiq workers chewing through three hundred million jobs a month, FastAPI services fronting ML inference, batch ETL in both ecosystems — and the gap is no longer arguable. Ruby’s AOT story, taken as the sum of mruby, Sorbet, Ruby 3.5’s YJIT, TruffleRuby, and now Spinel, is more coherent, more mature, and more deployable than anything Python has assembled despite a decade of effort and the largest developer mindshare in the industry.
I want to be precise about what I mean by “compilation story” because the phrase gets thrown around loosely. I mean the entire pipeline from source code to a binary or bytecode artifact you can deploy: type information, optimization passes, linking, packaging, startup time, runtime memory footprint, and the operational ergonomics of distributing the result. Python has more individual compilers than Ruby — Nuitka, Cython, Codon, Mojo, mypyc, PyPy, Pyston, Pyjion, Numba — but more is not better when none of them compose, none of them cover the full language, and none of them ship as the path of least resistance. Ruby has fewer compilers but they form a coherent stack. That is the entire thesis of this piece.
The Spinel announcement and why it matters more than another Python JIT post
Spinel, for those who missed the HN thread, is a new ahead-of-time compiler for Ruby that emits native code through LLVM, leveraging Sorbet type signatures to produce tight machine code for hot paths while falling back to interpretation for dynamic dispatch. The benchmark numbers in the announcement are real but not the headline I care about. What I care about is the architecture: Spinel does not replace MRI, it produces hybrid binaries that link the Ruby VM with compiled hot functions, much like how mruby has worked for embedded scenarios since 2014. The continuity matters. A Ruby team that has been writing Sorbet-typed code for the last three years can throw Spinel at it and get a compiled binary out the other end without rewriting anything.
I migrated our Sidekiq workers to mruby in 2024 — not all of them, just the hot ones that were burning CPU on JSON parsing and small-object allocation. The result was a 2.4x throughput improvement and a 60% reduction in resident memory per worker process. We went from twelve worker boxes to five and the savings paid for the engineering time in three months. That experience taught me something I had not appreciated about the Ruby compilation ecosystem: the implementations are designed to interoperate with the language as it is actually written. Mruby does not require you to learn a new dialect. Sorbet does not require you to abandon duck typing for the parts of your code where dynamism matters. YJIT does not require you to annotate anything. Spinel slots into this same philosophy.
Compare this to the Python landscape. Codon is fast — sometimes ten to one hundred times faster than CPython — but it implements a Python-like language, not Python. You cannot take an existing Flask app and run it through Codon. Mojo, which I respect tremendously as an engineering effort, is even further from Python than Codon despite Modular’s marketing. Mojo is its own language with Python-inspired syntax. Nuitka compiles real Python but produces binaries that are slower than CPython at startup and rarely faster at steady state. PyPy is a marvel but cannot run any package with a C extension that uses the C API in non-trivial ways, which excludes most of NumPy, all of TensorFlow, and a frustrating long tail of small libraries that businesses depend on. Cython is fantastic for hot inner loops but requires you to learn a third language sitting between Python and C. mypyc is closer to the Sorbet-plus-Spinel model but it is maintained by the mypy team as a side project and has never gotten the resources or the polish to be a primary deployment target.
The full table I have been refining for two years
Here is the comparison I keep updating as new compilers ship. I have used every one of these in either a production capacity, a serious prototype, or a benchmark suite where I controlled the methodology. The numbers in the performance column are geometric means across a benchmark suite I maintain that includes JSON serialization, regex-heavy text processing, integer arithmetic loops, hash table churn, and HTTP request handling — basically the workloads that make up real backend services rather than synthetic microbenchmarks.
| Language | Compiler | Type | Maturity | Perf vs interpreter | Deployment story | Production fit |
|---|---|---|---|---|---|---|
| Ruby | mruby | AOT bytecode + native | Mature (since 2014) | 1.5x to 3x | Single binary, embeddable | Excellent for workers, embedded |
| Ruby | YJIT (3.5) | Tracing JIT | Mature | 1.7x to 3.5x | Built into MRI, zero config | Excellent for web services |
| Ruby | TruffleRuby | JIT on GraalVM | Mature | 3x to 8x | JVM-based, AOT via native-image | Good for long-running services |
| Ruby | Spinel | AOT via LLVM + Sorbet | New (2026) | 2x to 6x claimed | Native binary, hybrid VM link | Promising, early adopters |
| Ruby | Sorbet Compiler | AOT for typed Ruby | Stable (Stripe internal) | 2x to 4x | .so loaded by MRI | Excellent if you have Sorbet |
| Python | CPython 3.13 JIT | Copy-and-patch JIT | Experimental | 1.05x to 1.3x | Built in, opt-in flag | Marginal, watch this space |
| Python | PyPy | Tracing JIT | Mature | 2x to 7x | Drop-in until C extensions | Pure Python only, niche |
| Python | Nuitka | AOT to C | Mature but slow-moving | 1.0x to 1.5x | Standalone binary | Distribution, not perf |
| Python | Cython | AOT to C, annotated | Very mature | 10x to 100x on annotated | .so modules | Hot paths, requires rewriting |
| Python | mypyc | AOT for typed Python | Used by mypy itself | 1.5x to 4x | .so modules | Limited adoption, niche |
| Python | Codon | AOT, Python-like | Stable but separate language | 10x to 100x | Native binary | Numeric code, not real Python |
| Python | Mojo | AOT, Python-superset | Developing | 10x to 1000x on Mojo code | Native binary | ML kernels, not your Django app |
| Python | Numba | JIT for numeric code | Mature | 10x to 100x on numeric | Inline decorators | NumPy hot loops |
Read the table carefully. The Ruby column is short because Ruby has converged on a small number of well-funded, well-integrated compilers. The Python column is long because Python has not converged. Length is a weakness, not a strength, when you are choosing what to deploy.
Why mruby beat anything Python has for embedded and worker scenarios
Mruby is twelve years old and most Python developers I talk to have never heard of it. That is a marketing failure on Matz’s part, not a technical one, because mruby solves a problem that Python has not solved at all: how do you ship a Ruby program as a single self-contained binary, with predictable memory usage, that starts in under fifty milliseconds, that you can embed inside a C application or a game engine or an IoT device. Python’s answer to this is PyInstaller, which produces hundred-megabyte zips that extract themselves at startup and take seconds to boot. Or Nuitka, which produces real binaries but does not meaningfully reduce the runtime memory because the Python interpreter is still in there.
Mruby is built differently. The bytecode is compiled offline. The runtime is a few hundred kilobytes. The garbage collector is a simple mark-and-sweep that you can configure or replace. There is no GIL because mruby gives you VM instances you control. I have a Sidekiq replacement we wrote in-house that runs mruby workers as separate processes, each consuming around eight megabytes of resident memory at idle versus roughly seventy for an MRI worker doing the same job. When you are running a thousand worker processes across a fleet, that difference is the difference between needing forty machines and needing seven.
The closest Python equivalent in spirit is MicroPython, but MicroPython does not run pip packages and its compatibility with CPython semantics is even more partial than mruby’s compatibility with MRI. The mid-range — embeddable, fast, real-language — does not exist in Python the way it does in Ruby. This is what I mean by Ruby’s compilation story being more coherent. The pieces are not arranged accidentally. Matz designed Ruby with mruby in mind. Sorbet was designed with the compiler in mind. YJIT was designed to be invisible. Spinel is designed to slot into the existing ecosystem rather than fork it.
Sorbet is the secret weapon Python’s type system never became
Stripe released Sorbet in 2018 and at the time the consensus on Hacker News was that it was too late, that Python had won the type-annotated dynamic language game with mypy and PEP 484, that nobody would adopt a third-party type system for Ruby. The consensus was wrong. Sorbet has gone from strength to strength because it does things mypy will probably never do: it has a runtime checker that actually enforces types at runtime, it has gradual adoption with strictness levels per file, it has tooling that stays out of your way, and it has a compiler companion that turns typed code into native code. mypy has none of these. PEP 484 types are a documentation system that happens to be machine-checkable. Sorbet types are a system you can build a compiler against.
This matters for Spinel specifically. The reason Spinel can claim 2x to 6x speedups on real Ruby code is that it has type information to work with. Generating fast machine code for a fully dynamic language is incredibly hard — that is why V8 has spent fifteen years and hundreds of millions of dollars on it. Generating fast machine code for a language that is mostly dynamic but has type information at function boundaries is much, much easier. Sorbet has been doing the cultural work for seven years, getting Ruby teams to add types. Spinel inherits that work. mypyc tries the same play in Python but the cultural work was never done because mypy stayed timid about runtime enforcement, kept evolving the syntax in incompatible ways, and never got the resources of a Stripe-class company committed to making the compiler real.
I run Sorbet at strictness level “true” across our entire Ruby monorepo, with about thirty percent of the code at “strict” and the truly hot paths at “strong.” Strong-mode Sorbet code is essentially statically typed Ruby — every variable, every method signature, every return value. This is the kind of code Spinel chews through and produces excellent machine code for. The same exercise in Python would require me to use mypy in strict mode (fine), then run mypyc against it (works but rough), then deal with the fact that half my dependencies have incomplete type stubs and the other half have type stubs that lie. The ecosystem is not there.
YJIT in Ruby 3.5 closed the JIT gap that everyone was waiting on
I want to spend some time on YJIT because the comparison with CPython’s new copy-and-patch JIT is instructive. YJIT was merged into Ruby 3.1 in 2021. By Ruby 3.3 it was production-ready for Rails apps and Shopify was running it at scale on tens of thousands of servers. By Ruby 3.5, which shipped late 2025, YJIT had matured to the point where the question is no longer “should I turn it on” but “is there any reason not to.” It is on by default in Rails 8.1. The performance numbers in production are 1.7x to 3.5x for typical Rails workloads, with ninety-eighth percentile latency improving by even more because YJIT specifically targets warm-path performance.
CPython’s JIT, by contrast, shipped experimentally in 3.13 and is still experimental in 3.14 as of this writing. The performance gains are in the 5% to 30% range. The difference is not because the CPython team is less talented — they are exceptional — it is because the architectural decisions made decades ago in CPython make JIT compilation harder. The bytecode is too high-level. The C API is too thoroughly committed to particular object representations. The reference counting interacts badly with optimizations that JITs typically rely on. CPython is paying down architectural debt that Ruby paid down years ago when it switched from MRI 1.8’s tree-walking interpreter to YARV in 1.9.
This is an important point that gets missed in language wars. Ruby quietly went through a years-long, painful transition from a tree-walking interpreter to a bytecode VM in the 1.8-to-1.9 era. The community survived it, libraries adapted, and the result was a runtime that could host modern compilation techniques. Python’s equivalent transition has been the long road from CPython’s stack-based bytecode to something more amenable to JIT, and it is still in progress with PEP 659 specializing adaptive interpreter work and the 3.13 JIT. By the time Python finishes that journey, Ruby will have spent a decade ahead of it on the optimization curve.
TruffleRuby and the option Python does not have
TruffleRuby deserves its own section because it is the implementation Pythonistas often wave away as “but JVM” without appreciating what it offers. TruffleRuby is a Ruby implementation built on the GraalVM platform that uses partial evaluation and aggressive inlining to produce code that, on long-running workloads, often beats both YJIT and the original Sorbet compiler. The benchmark game numbers put TruffleRuby in the same performance class as Java for some workloads, which is to say within striking distance of C for steady-state CPU code.
The catch with TruffleRuby has historically been startup time, because GraalVM JIT compilation takes time to warm up. The fix to that catch is GraalVM’s native-image, which produces an AOT-compiled binary from JVM bytecode. TruffleRuby plus native-image gives you a Ruby binary with sub-second startup, no JVM dependency, and steady-state performance close to TruffleRuby’s JIT mode. This is a deployment story Python simply does not have an analog for. There is no “PyPy plus native-image” combination that gives you a single-binary deploy with PyPy performance and no startup tax. Graal-Python exists but lags TruffleRuby in maturity, partly because the dynamism of CPython’s C API is harder to model than Ruby’s MRI extension API.
Concrete numbers from a workload I migrated last quarter
Let me ground this in a specific workload. Last quarter I migrated a webhook ingest service from Python to Ruby — not because of compilation specifically, but because we already had Ruby ops expertise and the Python service was getting hard to scale. The service does roughly two hundred thousand webhooks per minute at peak: receive HTTP, validate signature, enqueue for downstream processing. The Python version was FastAPI on uvicorn with gunicorn workers. The Ruby version is Roda on Puma with YJIT enabled.
The comparable numbers, normalized to single-instance throughput on identical hardware (c7i.4xlarge, sixteen vCPU):
- FastAPI on CPython 3.13, no JIT: 14,200 req/sec, p99 latency 38ms, 4.1 GB resident memory across workers
- FastAPI on CPython 3.13, JIT enabled: 15,100 req/sec, p99 latency 35ms, 4.4 GB resident memory
- FastAPI on PyPy: did not get this working because of dependency C extensions, abandoned after a day
- Roda on Ruby 3.4, no JIT: 11,800 req/sec, p99 latency 41ms, 1.9 GB resident memory
- Roda on Ruby 3.4, YJIT enabled: 22,900 req/sec, p99 latency 19ms, 2.1 GB resident memory
- Roda on Ruby 3.5 RC, YJIT 3.5: 26,300 req/sec, p99 latency 16ms, 2.0 GB resident memory
The comparison that matters is the third versus the sixth. Same workload, comparable framework idiomaticity, same hardware. Ruby with YJIT delivered 1.74x the throughput at less than half the memory. The Python JIT delivered a 6% throughput improvement. This is not an outlier in my experience; this is how the numbers consistently land for medium-complexity backend services. It is also worth noting that I could not get PyPy working at all because of incompatible dependencies. If you have read every essay arguing that Python is faster than Ruby in 2026, those essays are usually citing numeric benchmarks where NumPy is doing the work, or cherry-picking microbenchmarks where the GIL does not appear, or running on workloads that do not reflect actual web service patterns.
Why deployment ergonomics are the unspoken half of the story
Performance numbers grab the attention but I want to talk about deployment because it is where my Ruby-versus-Python experience diverges most strongly. Deploying a Ruby service in 2026 looks like this: you have a Gemfile, you run bundle install in a Docker build, and you ship a container. If you want compiled bytecode, you add bundle exec bootsnap setup and ship the cache. If you want JIT, you flip an environment variable. If you want native, you reach for Spinel or mruby or TruffleRuby native-image. Each step is incremental. Each step is reversible. Each step is composable with the others.
Deploying a Python service in 2026 looks like this: you choose between pip and poetry and pdm and uv and rye, you fight with virtual environments, you decide whether to use Docker or just ship a wheel, you discover that your wheel does not work on your deployment target’s glibc version, you switch to manylinux wheels, you discover that one of your dependencies does not ship a manylinux wheel, you build it from source, you discover that the build requires a specific version of OpenSSL, you eventually get it working, and then if you want any kind of compilation you start over with a different tool that imposes its own packaging model on top of the one you already chose. Nuitka has its own way. PyInstaller has its own way. Mojo has its own way. Codon has its own way. None of them compose with each other, and most of them do not play well with the standard pip ecosystem.
This is not a packaging digression — it is the heart of why Ruby’s compilation story works. The compilation tools live within the package and dependency ecosystem. They are not external systems trying to take over. Spinel will read your Gemfile and your Sorbet sigs and produce output. Mruby compiles bytecode that links into mrbgems, which is the embedded analog of rubygems. Bootsnap caches load paths and compiled bytecode in a way that any Ruby app can adopt invisibly. The tools cohere because the ecosystem has cultural expectations that they should cohere.
The Mojo question, addressed honestly
Whenever I make this argument someone brings up Mojo. Mojo is what Modular has been building for three years now, a language that is a superset of Python — actually getting closer to that promise as of the recent releases — that compiles to native code with performance that beats C in some kernels because of MLIR-based optimization and explicit value semantics. Mojo is genuinely impressive. I have written Mojo. The kernels I wrote ran faster than any Ruby code could. So why am I not bullish on Mojo as Python’s salvation?
Three reasons. First, Mojo is not Python despite the marketing. The parts of Mojo that make it fast — fn versus def, value semantics, struct types, explicit ownership — are all parts that are not in Python. You cannot port your Django app to Mojo by changing the import statements. You can call Python from Mojo, which is great, but that means Mojo is to Python what Cython is to Python: a separate language that interoperates well. Cython has been around for fifteen years and has not displaced Python despite having the same value proposition.
Second, Mojo is closed-source where it matters. The standard library is open. The compiler is not. Modular’s licensing has improved but the project is still controlled by a single company in a way that the Ruby compilers I am comparing it to are not. Sorbet is Apache-licensed. mruby is MIT. Spinel is MIT. TruffleRuby is EPL. CRuby and YJIT are BSD-style. The Ruby compilation ecosystem is collectively owned in a way that the Mojo ecosystem is not, and that affects long-term bets.
Third, Mojo’s natural application is ML kernels and high-performance numeric code, not webhook ingest or Sidekiq job processing. For those numeric kernels the comparison should be against Codon, against Numba, against C, against Rust — and against those competitors Mojo has real and unique advantages. But the article I am writing is about backend services and the kind of Ruby code that ships at 99% of Ruby shops, not about ML kernels. For the work most Ruby shops and Python shops actually do, Mojo is not in the picture.
What about Codon?
Codon is the most underrated entry in the Python compilation table because, unlike Mojo, it does not advertise itself as a Python superset. It is honest that it is Python-like. The honesty is appreciated and the performance is real. Codon code can run fifty to two hundred times faster than CPython on numeric workloads. The catch — there is always a catch — is that Codon supports a subset of Python and a small subset of pip packages. Anything that depends on the C API is out. Anything that uses dynamic features Codon cannot statically prove is out.
I tried to port a real Python service to Codon last year as an experiment. After two days of work I had ported maybe forty percent of the code, with the remaining sixty percent depending on libraries that did not have Codon equivalents. The exercise ended there. By contrast, when I tried Spinel against a Ruby service of similar complexity using the early-access version Stripe shared, the work was an afternoon. The Ruby code, including its gems, mostly just worked, with Sorbet types the only thing I had to add for the hot paths to actually compile to native.
A small Spinel example, for those who want to see code
Here is the kind of Sorbet-typed Ruby that Spinel can compile to native code with minimal scaffolding. This is from a JSON validation hot path I migrated last month.
# typed: strong
class WebhookValidator
extend T::Sig
sig { params(payload: String, signature: String, secret: String).returns(T::Boolean) }
def self.verify(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
Rack::Utils.secure_compare(expected, signature)
end
sig { params(json: String).returns(T::Hash[String, T.untyped]) }
def self.parse(json)
JSON.parse(json)
end
end
That code, run through Spinel with the right flags, produces a native object file that is loaded into the Ruby process and replaces the interpreted version. The HMAC call still goes through OpenSSL because that is a C extension, but the surrounding plumbing — argument marshaling, return value boxing, type checks — is all native. On the validator microbenchmark this took the per-call overhead from 14 microseconds to 4 microseconds, more than a 3x improvement on a function that is called millions of times a day.
The equivalent Python code, even with mypy types and even compiled with mypyc, would not get the same treatment. mypyc cannot compile across module boundaries the way Spinel can. mypyc cannot leverage runtime type narrowing the way Sorbet does. The output is faster than CPython, yes, but not 3x faster on this kind of code, and the build pipeline is rougher.
Counterarguments I take seriously
I want to engage with the strongest counterarguments rather than strawman the opposition. The one I take most seriously is mindshare. Python has more users than Ruby by an order of magnitude. The library ecosystem is larger. The job market is larger. New developers learn Python first. ML and data science are Python’s home turf and that has cascading effects across hiring and library development. Even if Ruby has the better compilation story, the gravitational pull of Python’s ecosystem might mean it does not matter — Python will eventually fund itself out of any technical hole.
I think this argument is correct in the long run and wrong in the medium run. Long run, fifteen to twenty years, Python will probably solve compilation through some combination of CPython JIT improvements, Mojo or its successors, and a slow consolidation of the fragmented compiler landscape. Medium run, the next three to five years, the gap will widen rather than narrow because Ruby’s existing tools are continuing to improve and Python’s are still trying to find a unified path. If you are choosing a stack today for a service that needs to ship fast and run efficiently, the medium-run reality is what matters.
The second counterargument is that compiled performance does not matter for most services because they are I/O bound, not CPU bound. This is sometimes true but increasingly less so. The rise of in-process ML inference, edge deployment, and embedded scenarios has pushed compilation back into the conversation for backend services. Even traditional web services benefit from compiled performance through tail latency improvements that translate directly to better user experience. The 19ms p99 versus 35ms p99 difference I cited above is a real difference for users, not a microbenchmark abstraction.
The third counterargument is that I am cherry-picking workloads and frameworks. Maybe. I do work in a particular niche and my benchmarks reflect that niche. But the niche I work in is what most backend developers actually do — webhooks, queues, APIs, batch jobs — and the benchmarks are reproducible. I publish my benchmark suite on GitHub for anyone who wants to argue with the methodology.
The compilation story is not about whether you can compile a hello-world program. It is about whether the compiler integrates with the way real teams ship real software. Ruby’s tools do. Python’s tools, in aggregate, do not.
Where Python still wins, in fairness
Lest this read as a Ruby evangelism piece, let me be clear about where Python wins and where I will continue to use Python. Numeric and scientific computing are Python’s. NumPy, SciPy, JAX, PyTorch, and the entire ML stack live in Python and the compiled story for them is excellent — Numba, Cython, JAX’s XLA, PyTorch’s TorchScript and torch.compile. If you are doing ML, Python is the right answer and the compilation story for the parts that matter — the kernels and the inference paths — is mature and well-funded.
Data engineering and analytics are largely Python’s. Pandas, Polars, DuckDB’s Python bindings, the entire dbt ecosystem. Ruby has nothing comparable. If you are building data pipelines, you should use Python.
Scripting and automation where you need a wide library ecosystem are often Python’s. The pip ecosystem may have packaging warts but it has libraries for everything, and for many ad-hoc scripts the library availability dominates the language quality.
What I am specifically arguing is that for backend services — web APIs, job queues, microservices, embedded scripting — Ruby in 2026 has a better story than Python, and that the gap is widening rather than closing because Ruby’s compilation tools compose where Python’s do not.
How I would approach a green-field decision in 2026
If I were starting a new backend service today, here is the decision tree I would use. If the service is primarily ML inference or data processing, Python with Mojo for kernels or Codon for numeric paths. If the service is a typical CRUD API, Ruby on Rails with YJIT, with Sorbet adopted from day one and Spinel held in reserve for hot paths. If the service is a high-throughput worker that runs CPU-bound transforms, Ruby with mruby or Crystal, which is a different conversation but a compelling one. If the service needs to embed in a host application, mruby. If the service is a CLI tool that needs to ship as a binary, Ruby with TruffleRuby native-image or, if it must be Python, Nuitka with the understanding that startup will be slow.
The thing I would not do — and this is a change from what I would have said three years ago — is reach for FastAPI plus uvicorn plus gunicorn as the default. That stack is fine, it is fast enough for most things, but it sits in the middle of the performance and ergonomics axes without being best at either. Rails plus YJIT is faster, easier to operate, and has a more polished compilation roadmap if you ever need it.
The cultural angle nobody wants to talk about
One more thing before I get to predictions. There is a cultural difference between the Ruby community and the Python community that affects how compilation tools get built, and I think it is worth naming. The Ruby community has historically been comfortable with opinions. Rails was famously opinionated. Sorbet was opinionated. Bundler was opinionated. The community’s response to opinions is generally to either adopt them or to fork and build something different — but not to argue endlessly about which approach is correct.
The Python community is much more pluralistic. There are six packaging tools because there are six legitimate opinions about packaging and the community has not been willing to crown one. There are ten compilers because there are ten legitimate research directions. This pluralism is a strength when you are exploring, but a weakness when you are trying to ship. Ruby’s compilation story benefits from the community’s willingness to consolidate around a small number of tools. Python’s compilation story suffers from the community’s reluctance to do the same.
I am not saying Ruby is right and Python is wrong. I am saying these cultural traits have real consequences for the tooling that ships, and if you are evaluating compilation stories you should account for the cultural axis as well as the technical one.
Predictions for 2026 and 2027
Here is where I commit to specifics. Take this as one practitioner’s read on the trajectory and adjust your priors accordingly.
For the rest of 2026: Spinel will not become production-default within the year, but it will be running in production at three to five major Ruby shops by end of year, including at least one of Shopify, Stripe, GitHub, or Square. The Sorbet team will release tighter integration with Spinel, possibly merging Sorbet’s existing compiler effort into the Spinel codebase. YJIT 3.5 will become the default for Rails 8.2 and Sidekiq 8. The Python side will see CPython 3.14 with the JIT graduating from experimental to default-on, but the throughput improvements will remain modest, in the 10% to 25% range for typical workloads. Mojo will release 1.0 and the marketing will be loud, but adoption will remain confined to ML use cases.
For 2027: TruffleRuby with native-image will ship as a recommended deployment for Ruby CLIs and serverless functions, displacing some Crystal use cases. Mruby will see a 4.0 release with improved Ruby 3.x compatibility, making it possible to run more vanilla Ruby code without modification. Spinel will reach 1.0 and start to be considered alongside YJIT as a default tool. On the Python side, the JIT will continue improving and PEP 703 (no-GIL) will move toward becoming default in CPython 3.16, which is the change I am most excited about for Python — not because of compilation, but because it unlocks the parallelism story that Python has been missing. Mojo will get traction in ML pipelines but the unified Python compilation story will not materialize because Modular’s commercial incentives diverge from the broader Python community’s needs.
For the language war narrative: I expect Ruby’s mindshare to continue shrinking in absolute terms while its quality-per-developer metrics improve, which is fine. Ruby is no longer trying to be the most popular language. It is trying to be the best language for the niche it actually occupies, which is opinionated, productive, well-engineered backend services. The compilation story is part of how it stays in that niche and arguably improves its position there. Python will continue being the most popular language by a wide margin. The two facts are compatible.
For specific shipping milestones I expect: Spinel 0.5 by Q3 2026, Spinel 1.0 by Q2 2027. Sorbet at 90% adoption among Ruby teams with more than ten engineers by end of 2027, up from roughly 60% today. Mruby 4.0 by Q4 2026. TruffleRuby native-image as a documented deploy target in the official Ruby docs by Q1 2027. CPython 3.14 released October 2026 with JIT default-on. Mojo 1.0 by mid-2026 with a serious push for Python-superset compatibility that lands somewhere between marketing claims and reality. PyPy 8.0 by end of 2026 with improved C extension compatibility but still well short of “drop-in replacement for CPython.”
Finally, a meta-prediction: the conversation about which language is “faster” will become less productive as both ecosystems converge on broadly similar performance ceilings through different routes. The conversation that will replace it is about which ecosystem ships better operational ergonomics, which has cleaner deployment stories, which lets a small team move faster without accumulating technical debt. On those axes, Ruby is winning today and I expect it to keep winning for at least the next two years.
Closing thought
Spinel’s HN appearance today is not the headline event. The headline event is the cumulative reality that Ruby in 2026 has YJIT, mruby, Sorbet, TruffleRuby, the Sorbet compiler, and now Spinel — six interlocking tools that together form a compilation pipeline more coherent than anything Python has assembled despite having ten or more individual compilers. The Ruby community organized around a small set of well-funded efforts that compose. The Python community fragmented across many efforts that do not. That difference, more than any individual benchmark, is why I think Ruby has lapped Python on compilation. And it is why, the next time someone asks me which language to use for a backend service, I will keep saying Ruby — not out of nostalgia, but because the tooling is better and the trajectory is clearer. Try Spinel when it stabilizes. Run YJIT today. Add Sorbet types if you have not. The future of compiled dynamic languages is here, and it is wearing a red gem.




Discussion