Ruby 4.0.0
Key topics
The Ruby community is abuzz with excitement as Ruby 4.0.0 is released, with many commenters expressing gratitude to Matz and the team for making their Christmas merrier. As the discussion unfolds, it becomes clear that some features, like Ractor, are still considered work-in-progress, while others, such as Fiber, have matured significantly and are being utilized in production environments, like Shopify's codebase. Notably, some commenters, like shevy-java, feel that the concurrency landscape in Ruby remains messy and could benefit from simplification. Meanwhile, others, like byroot, argue that Ractor is nearly ready for prime time, having resolved most of its known bugs.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
43m
Peak period
57
0-6h
Avg / period
14.5
Based on 160 loaded comments
Key moments
- 01Story posted
Dec 24, 2025 at 11:13 PM EST
9 days ago
Step 01 - 02First comment
Dec 24, 2025 at 11:55 PM EST
43m after posting
Step 02 - 03Peak activity
57 comments in 0-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Dec 28, 2025 at 1:11 PM EST
5d ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
I vaguely remember reading Shopify is using Fiber / Rack / Async in their codebase. I am wondering if Rails will get more Fiber usage by default.
It's a bit of a mess IMO. I'd much prefer everything be simplified aggressively in regards to threads + GIL; and Ractors integrated on top of Ruby::Box to provide not only namespaced container-like entities but also thread-support as a first-class citizen at all times. The API of ractors is weird and really not fun to use.
I wanted to read the source package files directly because I always found `shared-mime-info`'s usual two-step process for adding or editing any of the XML type data to be annoyingly difficult and fragile. One must run `update-mime-database` to decompose arbitrarily-many XML packages into a set of secondary files, one all-file-extensions, one all-magic-sequences, one all-aliases, etc. System package managers usually script that step when installing software that come with their own type data. I've accidentally nuked my entire MATE session with `update-mime-database` before when I wanted to pick up a manual addition and regenerated the secondary files while accidentally excluding the system path that had most of the data.
I ended up doing it with four Ractors:
- a Ractor matching inputs (MIME Type strings, file extensions, String or Pathname or URL paths for sniffing) against its loaded fully-formed type definition objects.
- a Ractor for parsing MIME Type strings (e.g. "application/xml") into Hash-keying Structs, a task for which the raw String is unsuitable since it may be overloaded with extra syntax like "+encoding_name" or fragment ";key=value" pairs.
- a fast XML-parser Ractor that takes in the key Structs (multiple at once to minimize necessary number of passes) and figures out whether or not any of those types are defined at all, and if so in which XML packages.
- a slow XML-parser Ractor that takes the same set of multiple key Structs and loads their full definition into a complete type object, then passes the loaded objects back to the matcher Ractor.
The cool part of doing it this way is that it frees up the matcher Ractor to continue servicing other callers off its already-loaded data when it gets a request for a novel type and needs to have its loader Ractors do their comparatively-slow work. The matcher sets the unmatched inputs aside until the loaders get back to it with either a loaded type object or `nil` for each key Struct, and it remembers `nil`s for a while to avoid having to re-run the loading process for inputs that would be a waste of time.
I think a lot of the barrier to entry for Ractors isn't the API for the Ractors themselves but in figuring out how to interact with Ractorized code from code that hasn't been explicitly Ractorized (i.e. is running in the invisible “main” Ractor). To that end I found it easiest to emulate my traditional library API by providing synchronous entry-point methods that make it feel no different to use than any other library despite all the stuff that goes on behind the scenes. The entry methods compose a message to the matcher Ractor then block waiting for a result or a timeout.
I also use Ractors in a more lightweight way in my UUID/GUID library where there's a Ractor serving the incrementing sequence value that serves as a disambiguator for time-based UUIDs in case multiple other Ractors (including invisible “main”) generate two UUIDs with the same timestamp. Speaking of which, I'm going to have to work on this one for Ruby 4.0, because it uses the removed `Ractor.take` method.
The Ractor experimental status could almost be removed. They no longer have known bugs, and only one noticeable performance issue left (missing Ractor local GC).
But the API was just recently changed, so I think it's better to wait another years.
> I vaguely remember reading Shopify is using Fiber / Rack / Async in their codebase.
Barely. There is indeed this management obsession for fibers even when it doesn't make sense, so there is some token usage there and there, but that's it.
There is one application that was converted from Unicorn to Falcon, but falcon isn't even configured to accept concurrent requests, the gain is basically 0.
As for Rails, there isn't much use cases for fibers there, except perhaps Active Record async queries, but since most users use Postgres and PG connections are extremely costly, few people are using AR async queries with enough concurrency for fibers to make a very noticeable difference.
[1] https://x.com/igrigorik/status/1976426479333540165
They indeed replaced Unicorn by Falcon in one application, but falcon is configured in "Unicorn mode" (no concurrent requests). So the gain is effectively 0.
Also note how they don't share any performance metrics, contrary to https://railsatscale.com/2023-10-23-pitchfork-impact-on-shop...
There's a pretty battle tested tool to define inline types as ruby syntax and type check both statically and at runtime[0].
It's still not a particularly nice situation imvho compared to typescript or python, but there's been some movement, and there's a newsletter that follows static typing developments [1] which may give you some insights.
0: https://sorbet.org/
1: https://newsletters.eremin.eu/posts
Really rough around the edges, lots of stubs have to be added because support for gems is lackluster but whatever Sorbet generates are hit or miss etc. So you end up writing a lot of hard to understand annotations and/or people get frustrated and try to skip them etc.
Overall a very bad DX, compared to even typed Python. Don’t even want to compare it to TS because then it becomes really unfair.
Python is one of the Lingua Franca of scientific, data, and most importantly, ai communities
Has much bigger community than Ruby
Has much, much better tooling story
Has much better gradual typing story
Isn’t THAT much slower than Ruby, there are far more attractive targets than Ruby if you care about performance
You mean it's the most used frontend for all the C and C++ libraries that are used for scientific computing, data and AI.
Bah, humbug!
Speed isn’t why people choose Python.
IMHO if we wanted to write types in our programming language we would not have chosen Ruby for our programming tasks. We would have chosen one of the zillion of other languages. There were a lot of them when Ruby got traction about 20 years ago and many other languages have been created after then. It's not surprising that one of the main proponent of typing in Ruby is Shopify, because their path away from Ruby is very costly.
In my case one of the reasons I invested in Ruby is precisely because I did not have to write types.
Does it make Ruby slower than Java, my main language in 2005? Yes.
Is it fast enough for my customers? Yes.
Do I have to write unit tests to check for types? I don't.
Occasional problems that static types would have prevented to happen? Once or twice per year. Overall that's a good tradeoff because pleasing the type checker for non trivial types can be a time consuming task and some errors happen at runtime anyway, when the real world hits with its data a carefully type checked code base. Think of an API suddenly returning a bad JSON. Static or dynamic typing, both won't help with that.
For generating (with LLMs) API clients and CLIs it’s especially useful—define the shape once, get validation at ingress/egress for free.
Maybe momentum is happening in new projects rather than retrofits? [0] https://oss.vicente.services/dspy.rb
I’ve been using this pattern for API clients[0] and CLIs[1]: define the shape once with Sorbet, get automatic JSON Schema generation when you need it.
[0] https://github.com/vicentereig/exa-ruby [1] https://github.com/vicentereig/lf-cli
Well, maybe next time.
Out of all three I think Shopify have the highest possibilities. There may be additional usefulness interms of ZJIT.
I like it, it deserves attention, especially for those who are seeking for typed Ruby. With this, you can finally experience it, and the syntax feels more ergonomic than with Sorbet.
I think most people who cared just moved to typescript.
Adding static typing to a dynamic language mostly gives you the disadvantages of both, without a lot of benefits. It's better to stick to languages that were designed with static types from the start.
I love programming in Ruby, having to worry about type annotations and the additional constraints that come with them would take a lot of the fun out of that.
I've been using Ruby for more than 10 years now, and I only started using LSP recently. To me it's a nice addition but I can live without it. Type is just one of the tools, not the only one imo. Not trying to sound negative but type is becoming more like a hammer analogy nowadays.
And it's not limited to Ruby. Javascript, Python, all similar languages. Not everyone is a fan of type. We won't reach consensus imo and that's ok.
In our codebase that uses Sorbet I find this is really only true at function boundaries. Within a function it is pretty rare that anything needs to be spelled out with inline annotations to satisfy the compiler.
If only private methods would be allowed not having typing at all (with a promise of not being used in subclasses, for example), and Sorbet would be used mostly on the public surface of classes, it'd be much more tolerable for me.
In other words, I found that the resulting code often looked more like Java but with weaker guarantees about types and much worse performance.
As an engineer at a firm doing heavy duty data pipelines and internal tooling in a Sorbet-ified codebase, I disagree pretty strongly. While Sorbet type signatures are never going to win a syntax beauty contest, they are more than worth their weight in the way I can rely on them to catch typing and nilability goofs, and often serve as helpful documentation. Meanwhile, the internal code of most functions I write still looks like straight Ruby, fluent and uncluttered.
A good CI story that leans on tapioca was crucial here for us.
Of course RBS makes this more explicit/verbose (in a good way), and ruby-lsp helps bring it all together in the editor.
I feel like I'm missing nothing compared to Python with type hints and pyright. Of course neither compare to an actually typed language at runtime, but at least as far as developer experience, it's pretty alright. I'm relatively new to Ruby but I went from really hating it to being pretty much fine with it for these reasons.
The ruby::box thing looks pretty interesting, from a cursory glance you can run two simultaneous versions of something like a feature or rollout much more conveniently.
Also being able to do
on multiple lines rather than one - this is pretty nifty too!I'm not actually in need of this feature at the moment, but it would be cool and I think it fits very well with the idea of ractors as being completely separated from each other. The downside is of course that sharing objects between ractors would get slower as you'd need to copy the objects instead of just sharing the pointer, but I bet that for most applications that would be negligible. We could even make it so that on ractor creation you have to pass in a box for it to live in, with the default being either a new box or the box of the parent ractor.
Ruby::Box wouldn't help reducing contention further, they actually make it worse because with Ruby::Box classes and modules and an extra indirection to go though.
The one remaining contention point is indeed garbage collection. There is a plan for Ractor local GC, but it wasn''t sufficiently ready for Ruby 4.0.
Assuming you mean "because with Ruby::Box classes and modules have an extra indirection to go though." in the second paragraph, I don't understand why that would be necessary. Can't you just have completely separate boxes with their own copies of all classes etc, or does that use too much memory? (Maybe some COW scheme might work, doodling project for the holidays acquired haha)
Anyway, very cool work and I hope it keeps improving! Thanks for 4.0 byroot!
Yes, Ractor local GC is the one feature that didn't make it into 4.0.
> Can't you just have completely separate boxes with their own copies of all classes etc, or does that use too much memory?
Ruby::Box is kinda complicated, and still need a lot of work, so it's unclear how the final implementation will be. Right now there is no CoW or any type of sharing for most classes, except for core classes.
Core classes are the same object (pointer) across all boxes, however they have a constant and method table for each box.
But overall what I meant to say is that Box wouldn't make GC any easier for Ractors.
I spent over a decade enjoying Ruby and even wrote a book about it. At this point, though, Python has won for me: fastapi, pytorch, langchain, streamlit, and so on and on.
It's a bit sad, but I'll always remember the Christmas gifts, and the syntax that is always so much better than Python.
For me, the killer feature of Python was the typing module and the intellij pycharm community edition being free and RubyMine having a subscription fee.
I'm hopeful that the incoming type system work makes me happier there, though I'd also prefer a nicer editor experience than is currently available.
I’ve seen this sentiment expressed numerous times and have never found it to be true in my own work (e-comm), do you mind mentioning _what_ type of domain your web apps are in?
We have so much boilerplate and tooling to share request/response types between services and it's just... heavy. The same feeling arises when I'm sitting here trying to share a shape between a web app and the backend service, where FINALLY I just want the types to get out of my way instead of having to go through all this ceremony.
And my domain is relatively precise and typeable - streaming video with a deterministic set of parameters.
Generally though I'm more likely to agree with the value of types than to undersell them; I just can't find a ways to describe the above experiences such that they reflect that perspective.
i am learning Elixir and liking the concepts. i am coming from kotlin/jvm and i like kotlin, apart from kotlin-coroutines. planning to migration all threading code to virtual threads. but biggest problem is threadlocal.
1. Hiring and job market - the JVM is simply more employable
2. Ecosystem - in general you can expect the JVM to have library support for most things you're going to need.
3. Typing - if you like static types, you're probably going to miss that in Elixir/Erlang. They're working on a gradual type system for Elixir that looks quite pragmatic, so I'm excited to see how that works out.
The Elixir side of things has some real advantages, though. Runtimes like the JVM are slowly adding threading paradigms that start to look like how the beam works, if you squint enough. Naturally, Elixir already has that, and already has technologies that work very well with it. Virtual threads on the JVM are a smart effort that will take a long time to be complete and will always have to take backwards-compatibility into account, especially if you're in Java itself.
Phoenix is also IMO a best-in-class web framework. I don't think it's universally applicable, but if any JVM language had something like Phoenix I doubt I'd be considering Elixir nearly as strongly (due to my affection for types). So while the JVM ecosystem is broader, it's not uniformly stronger.
I also think that "domain" is much broader for JVM stuff. The web technologies there feel pretty baroque rather than empowering, but you can still do web on the JVM, and Kotlin or Scala in particular IMO serve better for systems where the bulk of the code is internal business logic. I think that even if I adopted Elixir entirely, I'd probably retain some "second language" for deeper systems.
How do you know it? Today you have to re-assess what you've learnt in the past. If you think JVM is "simply more employable" and haven't tested the waters in last 1-2 years, chances are you're just wrong.
The feeling I have: more simpler the tech is (and JVM/Ruby/other CRUD), the highest salary cuts you're getting. And you're actually less employable.
It is that counter-intuitive, because it's one person with a coding agent vs a team in the past.
(I also come from Scala and so far, like what I see with Gleam.)
1. A niche within a niche is a bad idea 2. If you're going niche, going further niche hardly makes a difference
It's telling that your reasons for switching are all features of Python's ecosystem, not of the language itself. A lot of developers are moving to Python because of its libraries, and in many cases they don't care for the language at all.
That's causing a problem for Python: many of these developers who'd rather be using different languages seem to want to morph Python into their language of choice. The result is that the Python language is pulled in many different directions, and with each release gets increasingly bloated and strays further from its foundations.
Ruby, on the other hand, has a community that's mostly made up of people who actually like the language. That allows it to do a much better job of staying true to its core philosophy.
Why would you write something so clearly false?
I don’t have any specific complaints about Python syntax because I can force it to get the job done…but homogeneous, it is not.
It hasn't had any such language-changing release for at least six years. The implication that this is an ongoing process attributable to newer adopters is simply false. It is a process that has stopped, and when it was happening, it was attributable to longtime Python developers.
I think Guido left the BDFL role in 2018, and we’ve gotten walrus operators, structured matching, and exception groups since then (just off the top of my head). There’s also been significant language/grammar accommodations towards type annotations.
Overall, I’m of the opinion that Python’s language evolution has struck a pretty nice balance — there’s always going to be something new, but I don’t feel like the syntax has stagnated.
Right, because ecosystem beats syntax any day of the week. Plus many of us also think the Python language is nicer anyway. For me I can't get past Ruby's free wheeling approach to import scoping and tolerance for magic.
but I still love writing full stack webapp using rails so yeah
thats why I really love pyCall
The only thing missing are some profiling tools.
If you really need those you're better off with Rider which is integrated with DotTrace.
Luckily people seem to be aware of this and there was a whole talk about improving Ruby DX.
We created an Abstract controller that handles all of the typical behavior for a resource, auth, filtering, pagination, tenancy, import/export, serialization etc.
Then we expanded rails generators to cover ALL typical behavior. And the markdown file calls the generators.
It was a bit complicated to model polymorphic behavior but we got it working thanks to Ruby/Rails.
But the basic premise that made this work is: Use only restful actions; don’t turn it into RPC. Recognize that most RPC/graphql functions are state changes that could have been a patch request. So instead of /clients/activate its /clients with a status attribute for “activate” or “archive”. Then most nested routes aren’t needed, use accepts nested attributes for and return child ids in the show action. There’s more to it that this but by strictly following conventions and modeling the data for rest, the api ends up Super simple.
Our standard controller only whitelists strong params. All other behavior is automatic.
Client first_name last_name organization:references
Organization name tax_number has_many:Clients
Have you heard about Lisp?
I might have forgotten a (
Wrt "forgetting" parentheses, the Lisp people have ways around that: they set up their editors so they operate on the expressions and can't "forget" a paren. They operate on the logical blocks (which is easy (because of the parentheses)).
What's particularly exciting is how this positions Ruby for modern workloads. With proper parallelism, Ruby apps can finally compete with Go and Node.js in concurrent scenarios without sacrificing developer happiness.
The typing improvements also can't be understated. Gradual typing strikes the right balance - it helps teams scale codebases without forcing the verbosity of Java or the complexity of TypeScript's type gymnastics.
Looking forward to seeing how the Rails ecosystem adopts these features. This could spark a Ruby renaissance in 2025.
Is it against the rules to say a comment is clearly AI generated?
Box is for isolating definitions and has nothing to do with GIL.
In theory, maybie. Parallelism support isn't a boolean though as there's a lot of additional factors at play. Just as one example, late stage Visual Basic also got parallelism support, but it really didn't help the overall positioning of the the language among its peers.
For the usual doomsdaysayers saying "ruby can't X so I left it for Y", when X is typing, RBS is becoming the accepted standard (now that sorbet supports it),and RBS inline notation next to signature/code too (for peeps complaining about separate files); when X is LSP, ruby-lsp is the standard and already supports "go to definition" (its major hole for a long time), and its plugin architecture allows other other features to reuse the same code AST/index (So that each linter/formatter/type checker doesn't have to parse their own); when X is parallelism, ractors are have actually become performant in a lot of common cases, and it's only missing some GC improvements to be truly non-experimental.
There are new shiny things like ZJIT or Box, but even the core team recommends against using them in production for now. But they'll get better, as its been happening with the things listed above.
No wildly new syntax changes is also a good thing. Should help alternative implementations catch up.
Personally I can’t see any comment based typing system gaining real traction.
Adds type annotations to the core language syntax. The compiler does type checking, strips the annotations, and outputs plain Ruby.
Why is it important to be a separate layer that compiles to plain untyped Ruby?
Same reason Typescript was made and we didn’t add types to JavaScript.
And RBS is officially part of Ruby…
Type annotations in the language as syntax. Static type checker with an emphasis on inference. Compiles into Ruby so that it integrates with the entire existing Ruby ecosystem, so unlike Crystal as well.
Those are the general features you need/want and why TS caught on and none of the existing solutions hit the mark.
Sorbet is a bit different from TypeScript because it has runtime type checking, not just static.
It does occasionally require structuring your code differently, but I find the type-system-encouraged approach often gives a more elegant and harder-to-misuse interface in the end.
I spend my working life swapping between Ruby and typescript projects and the typescript project is utter garbage with poor test coverage that needs a day of human QA for every build whereas the Ruby project is well tested such that we know that CI passing means it’s good to be released.
(I also work in a 40m+ loc non-rails ruby codebase that is almost entirely typed with Sorbet.)
How so?
I never really missed types in Ruby, even if I like them a lot in typescript, but right now I'm doing some "vibe coding" on a personal project and I was thinking about trying Sorbet. I think that it could help Claude Code avoid some mistakes it often makes which make it waste a lot of time fixing.
When vibe coding, what I noticed is that CC tends to make mistakes which it does catch with tests and fix on its own, but my hope is that using Sorbet this will happen much less, and thus development will go faster with less (slow) test cycles.
I don’t want to downplay the work done by the maintainers on the contrary, huge thanks to them. But I do feel the version number is a bit misleading.
That said, the work on the ZJIT[1] compiler is massive. It’s serious, professional engineering, and definitely deserves respect.
[1]: https://rubykaigi.org/2025/presentations/maximecb.html
For a more concrete example, the grpc gem locks Ruby versions (< 3.5), and they refuse to change it. So until they support the next Ruby version, we could test ruby-next by testing with a preview release. This worked for 3.4 and 3.5, but now doesn't work with 4.0 (bundler resolves 4.0-preview2 > 3.5, whereas we are able to do 3.5-preview1).
So unless I feel like doing a lot of grunt work (which I don't), I can't even test Ruby 4 in our app until they release a new version. And while I recognize this is an issue with the gem, it is a consequence of choosing to do 4.0.
I am installing it now. Thank you Matz and team.
28 more comments available on Hacker News