Aaron Patterson on Ractors, Faster Gem Installs, and Why He Can't Fully Trust AI-Written Code

Open on YouTube ↗
Overview

In this episode of On Rails, host Robby Russell talks with Aaron Patterson, known online as Tenderlove, a senior staff engineer on Shopify's Ruby infrastructure team and a longtime member of both Ruby and Rails core. The conversation covers the low-level work Patterson's team does on the Ruby interpreter, including true multi-core parallelism with Ractors, a JIT compiler, and garbage collection. It then moves to a proposal to make gem installation much faster by changing how gems are named and addressed, a trick for letting a JIT compiler see through C extensions, and how Patterson uses AI day to day. Across the whole conversation, Patterson's stated goal is to make Ruby and Rails faster and easier to start with, without application developers having to change anything.

25 min read

Why Rails, after all these years

Asked what keeps him on Rails, Patterson gives two reasons. First, he says Rails is still the easiest way to build a website and get an app built. Second, Rails keeps up with advances in web technology. It is not a framework that was built once and then left frozen. He adds, half-jokingly, that he is "not good at building websites at all," and that Rails makes it easy even for someone like him.

His path into web development began in 1999, straight out of high school. A friend working at a car dealership needed a website, so Patterson built one. It was dynamic: Perl CGI scripts plus JavaScript, deployed by uploading files over FTP, with CVS for version control. He stayed about a year, but web development became his career. He was already writing Ruby as a hobby when he saw David Heinemeier Hansson's original "build a blog" Rails demo, and he remembers thinking, "This is it. This is what I got to do."

Patterson admits he is heavily biased, because he works almost entirely in Ruby and Rails. He has tried other stacks. Building apps with Node felt to him like "the ergonomics are absolutely not the same." He first tried Django, amusingly, at Rails World, and found it nice but missing many of the bells and whistles Rails includes. He hears PHP has improved a lot recently. His underlying preference is that he does not want to make these choices himself: "I really want somebody to just tell me like no this is what you got to do."

The Ruby infrastructure team's mandate

At Shopify, Patterson rarely works on the web applications themselves. His team focuses on the Ruby interpreter. He mainly works on the JIT compiler, and the team also works on concurrency, parallelism, and garbage collection. He estimates the group at roughly 40 people, covering Ruby and Rails internals as well as developer productivity. He also works with infrastructure engineers and application developers.

He describes the mandate as helping Shopify's applications make better use of its production infrastructure, which he admits is a vague goal. In practice it means fitting more requests onto each machine. That is less about hunting individual bottlenecks and more about architecture. He breaks the big picture into three parts:

  • Ractors address multi-core utilization: how to use every CPU on the machine.
  • The garbage collector addresses memory: how to use less of it.
  • The JIT compiler addresses single-core speed: when code runs on one core, how to make it run as fast as possible.

What he enjoys most about this work is making a system better without anyone having to do anything. "You upgrade your application, you didn't touch anything, and it's just faster and better." Breaking an API gets complaints, he notes, but nobody complains that an app became too fast. He likes seeing graphs move in the right direction. But he finds the constraint itself more exciting: improve performance while breaking zero people's code.

Ractors: true parallelism, and where they don't belong

Patterson explains Ractors as being like threads, except they really run in parallel. Two Ractors use two different cores. With ordinary threads, no matter how many you create, you are still effectively limited to one core. Ractors have existed for quite a while, but he says hardly anyone used them because they crashed a lot. Some workloads were also slower. He recalls a bug report where parsing a queue of JSON documents one at a time was faster than parsing them in parallel with Ractors, which he calls "clearly bad." Fixing problems like that has been a focus for his team. He now recommends that people start using Ractors, especially with Ruby 4.0, where he says they are fast and allow real parallel work.

He compares the model to Erlang or Elixir: a message-passing system in which you can send whatever you like, typically in a producer-consumer pattern such as a set of Ractors pulling JSON documents off a queue.

He is clear about where Ractors do not belong. He recommends never using them in the request/response cycle. In his view, it should be very uncommon for anyone to write Ractor.new, or even Thread.new, in a controller, view, or model, because a typical request just processes some data, renders a view, and sends a response. For database concurrency he points to Active Record's async queries, which already do that work in the background. Background jobs are a more plausible place for Ractors, but even there he expects the job processor to handle it for you.

The one strong use case he offers is OpenTelemetry. Collecting and logging application statistics usually happens in a background thread created with Thread.new. Because only one thread can use the CPU at a time, he explains, the app stalls while that thread runs. A Ractor would avoid this. Even so, he stresses that application developers would not be typing Ractor.new themselves.

Making Rails Ractor-safe

The Rails codebase itself does not use Ractors much. One of the team's current projects is making Rails Ractor-safe. The goal Patterson describes is that you can run rails new and run the resulting app inside a Ractor-based web server. Today, with any Ruby web server, multi-core parallelism requires multiple processes: 20 cores means 20 Ruby processes. With a Ractor-based server, one process could use all 20 cores. He thinks the server could detect the core count automatically, with an option to limit it.

He says this work is driven by Shopify's needs, and part of the demand comes from infrastructure engineers, who want a Ractor-based server because it would simplify their deployment strategy. His description of how projects get assigned is: "I have an idea, like okay, you have to do it now… own that, great."

Testing on edge Ruby with shadow traffic

The team always tests against edge Ruby. Sometimes that means working with application developers whose code does not run on edge Ruby and getting it fixed. The team has an on-call rotation, but Patterson says the load is manageable, largely because they have to be very confident before anything reaches production. If something fails there, it fails right away.

A key tool is Shopify's Storefront Renderer (SFR), which renders all Shopify storefronts and is read-only. The team deploys its own experimental build of SFR and replays production traffic through it, which Shopify calls "shadow traffic." Real customer requests still go to normal production servers. Because the app is read-only, replaying traffic cannot affect customers. The team can also compare the experimental output against the known-correct output. Patterson says anyone at Shopify can use this, not only for Ruby or Rails changes, whenever they want a layer of protection beyond tests.

The team rarely loads Shopify's applications locally for feature work. They do it to test the JIT, GC, or parallelism, and usually only after a lot of development on the JIT itself. Shopify's development environments are highly automated. Patterson calls the setup "pretty magic" for normal app developers, although his team has the unusual problem of having to modify Ruby's internals inside those environments. He does not know whether any of that environment tooling has been open-sourced; that belongs to a different team. Similarly, he runs rails new often, but only to create throwaway apps for testing Rails itself ("test app one, test app two"), not real projects.

Asked for the best database in the world, he improvises a tier list: SQLite is S-tier ("convenient, easy to use, deployed everywhere"), Postgres is A-tier with cool features, and MySQL gets a B. He does not hate it but thinks it has "weird quirks." When Robby asks whether those quirks still hold, Patterson admits he is disconnected. He used MySQL at GitHub and has no real complaints.

The goal: zero to Rails app, fast

The ecosystem friction Patterson cares most about right now is how quickly someone can go from nothing to a running Rails app. He reasons that faster setup means more people using Rails, and in a "TikTok culture," a slow install means "somebody's going to scroll."

He walks through the steps on a fresh laptop. You install Ruby, possibly compiling it along with dependencies such as OpenSSL, usually through a tool like mise, chruby, ruby-install, or RVM. Then gem install rails, then rails new, which runs bundle install. That is a lot of time spent installing. One improvement is shipping precompiled Ruby binaries, which he believes mise already does, although it does not help him because he always compiles edge Ruby. The other improvement is distributing Rails' native-extension dependencies as precompiled binary gems. His colleague Edward has been working on this and presented it at RubyKaigi. Patterson does not remember the exact percentage but calls the reduction "enormous." With every gem provided as a binary, he says, installation takes something like one or two seconds.

As for why this was not always the approach, Patterson thinks that in the past Rails simply depended on fewer native extensions, so it mattered less. Robby brings up long Nokogiri install times. Patterson jokes about who wrote Nokogiri and credits Mike's hard work shipping binary builds of Nokogiri, after which installs got much faster.

Why binary gems are hard: paths, fat binaries, and gRPC

Patterson describes several obstacles. The first is Ruby itself: compiling Ruby bakes absolute paths into the binary. If you compile Ruby into /foo and move it to /bar, it will work only sometimes. Since usernames and home directories differ between machines, a distributed binary Ruby has to cope with this. He believes mise handles it but is not sure how.

The second concerns the gems. A binary gem's filename has the form name, version, platform, for example Nokogiri for x86 Linux. The compiled .so file inside has to be linked against a specific Ruby version, but authors want one gem to work across Ruby 3.3, 3.4, and 4.0, and users only download one file. The current solution is the "fat binary": the gem contains one .so per supported Ruby version, and Ruby code in the gem picks the right one at require time. Patterson sees two drawbacks. Gems get bigger, so someone on Ruby 3.4 still downloads binaries for every other version. And when a new Ruby release comes out after a gem version has shipped, that gem version has no binary for it, and there is no way to add support to the already-published version.

Shopify runs into this constantly with gRPC. When a new Ruby version comes out, Shopify wants to upgrade but has to wait for Google to publish gRPC binaries for it. Patterson calls this "a huge problem that we have to overcome."

The proposal: content-addressable gems

Patterson says the team plans to tackle these problems one at a time, starting with the multiple-Ruby-version issue. In his view it "all boils down to literally the name of the gem file itself," because RubyGems and Bundler overload the filename with meaning, namely the platform. He wants the filename to carry no meaning and to work more like an address. Instead of addressing a gem by name, version, and platform, he proposes addressing it by content: name, version, and the SHA of the gem file.

Under that scheme, a gem author could publish one package containing only the Ruby 3.4 build, another for 4.0, and later one for 4.1. Each would be a distinct artifact containing only what that Ruby version needs. RubyGems.org would compute the SHA at publish time. Its version metadata, which already lists fields such as dependencies and required Ruby versions, could state that a given artifact requires exactly Ruby 4.0, so clients fetch the right one.

Patterson has pitched this to the RubyGems.org team. He says they seem to like it, and he thinks they will do it. The plan is a V2 API: old clients keep using V1, new clients use V2 and download the new artifacts. He expects it to be completely invisible to users: "hopefully, you just like upgrade Bundler and then it works." Asked about precedent, he says other ecosystems use content-addressable data, possibly Nix, but he is unsure of the details.

On containers, he does not expect much impact beyond faster builds, since a container image is built once anyway. The people he cares most about here are new users. Teams that already have container-based deployment have the resources to handle this, and he says those cases can be supported fine. New-user experience, he argues, keeps the Ruby and Rails ecosystems going and should be a high priority. (He also mentions that macOS still ships an old Ruby that warns you not to use it, and that it no longer includes Rails.)

Matz's Spinel and the bootstrapping problem

Patterson is excited about something Matz announced in a RubyKaigi keynote: Spinel, an ahead-of-time compiler that turns Ruby code into portable binaries. He imagines building RubyGems and Bundler, possibly combined into one tool, as a single precompiled binary, so installation becomes "download this package" with nothing to compile. He frames the core issue as a bootstrapping problem.

He says he still needs to dig into the project. As he understands it, Spinel removes the hard parts of Ruby. For example, it does not support eval, so metaprogramming that depends on eval will not work. He thinks that is probably an acceptable restriction for tools like RubyGems or Bundler. He sees Spinel as a separate project from Ruby's regular releases. More broadly, he notes that people have long switched to Rust or Zig ("I'm going to say Zig cuz I like Zig") for command-line tools, because distributing a Ruby CLI means making users install Ruby and deal with version mismatches. Shipping Ruby programs as executables would, in his view, "be awesome."

Why gems use native extensions, and "itch cases"

Patterson gives two real reasons gems use C extensions: speed, which he considers debatable, and the need to call a library written in another language. His cynical third reason is that authors "don't know any better."

Robby recalls writing Postgres functions in PL/Ruby that loaded RedCloth to render Textile directly from a query, as an example of using Ruby somewhere it doesn't belong just to see if it works. Patterson shares two of his own. SQLite lets you supply a virtual file system through function pointers that replace system calls like open and close. Patterson exposed that API to Ruby, so every time SQLite wanted to open a file, it called Ruby code. He used this first to build a self-contained script that stored its SQLite database in the __END__ data section of its own source file, modifying itself as data changed. The second, inspired by a suggestion from Chad Fowler, stored database data in HTML tables: rows in <tr> tags, cells in <td> tags. He says these are possible but not useful, and hesitates to call them "use cases." The two settle on "itch case" instead.

FFI, and letting the JIT see through C extensions

FFI is what Patterson calls one of his favorite itches, and it ties back to the JIT. His team's JIT compiler can only speed up Ruby code. Native code is opaque to it. So he wants people to write Ruby wherever possible, while acknowledging that calling an existing library like SQLite is a legitimate reason for native code.

FFI lets you call C libraries without writing C. You describe the function in Ruby (for example, a function named foo that takes an integer and returns an integer), and libffi sets up the call and connects the two sides. The drawback, Patterson says, is that it is very slow because it does a lot of work per call, which is why he has never used it in his own native extensions. He gave a whole talk on why FFI is slow, delivered in Japanese.

His project takes FFI declarations written in Ruby and generates a C extension from them, which gets compiled and is faster. That puts things back in C-extension territory, which he wants to avoid. The key step is that the generated C includes hints for the JIT compiler. Patterson says a hint is just metadata, such as "this function is named foo, it takes an int, it returns an int." When the JIT compiles code that calls these functions, it reads the hints, generates the native calls itself, and stops using the C extension.

He explains the motivation in terms of boxing and unboxing. Converting a Ruby integer to a C integer is unboxing, and the reverse is boxing. He wants the JIT to handle that type translation so it can profile and specialize its machine code for the actual types. A normal C extension is a black box: the JIT does not know what types go in or come out or what the extension does. The hints solve that. As for who should use FFI, Patterson's answer is only gem authors. As with Thread.new or Fiber.new, application developers shouldn't be writing it.

Should that even be a gem?

Asked when code should become a gem instead of living in the app, Patterson says the question is harder now because of LLMs. You can just ask Claude to build an integration instead of depending on someone's gem. His older answer still holds, though: anything outside your core business, such as a third-party API integration, is a good candidate to pull into a library. He frames this as architecture and maintainability rather than open source. A separate library can be tested independently and iterated on without moving in lockstep with the main app. Whether to publish it is a separate question.

How Patterson uses AI

Patterson uses AI "every day… all the time." Robby notes that the conversation was recorded in the second half of May 2026. Patterson's favorite use is exploring unfamiliar code. At a company with thousands of developers and many huge apps he has never seen, his work often requires jumping into one and figuring out what is going on, and he calls AI "the bee's knees" for that.

He also uses it heavily for hobby projects. He has small displays with an HTTP API that only accepts posted JPEGs. He wanted to generate images programmatically and found no suitable JPEG library, so he had Claude write a JPEG encoder, which he says he needs to release as an open-source gem. He also bought a small photo printer and suspected his computer's print path was altering colors, because prints didn't match his monitor. He had Claude build a small tool that uploads JPEGs directly to the printer over a socket, so he knows the exact bytes on his computer are what the printer receives. The prints still don't match. He says the real issues are that he needs an ICC calibration profile and has to map colors into the printer's narrower dynamic range.

The trust problem

The part of AI Patterson dislikes is something he is thinking about writing a blog post on, though he says his thoughts aren't fully formed. With a human teammate, he reviews the first pull requests carefully, then reviews less as he learns their work and comes to trust them. He says he has personally fallen into the same pattern with AI: it does the right thing, he checks carefully, it does the right thing again, and gradually he checks less, until it produces something bad that he doesn't notice. "You cannot treat it like a human," he says, but it is "close enough to trick you into thinking it can do a good job," and in some cases it just can't, with no signal beforehand.

Robby points out that human code quality can also decline, for example when someone is on the way out of a job. Patterson answers that with a person, you can talk to them and learn the context. With AI, the error rate seems to him essentially random, and the model is equally confident every time: "Oh, yeah, I totally did it. For sure." After one experience where AI got something badly wrong, he says he has "relegated" it to work he just doesn't want to do. Robby responds that those may be exactly the tasks that need the most attention. Patterson's rule for himself is that he must understand every line he submits, because he has to be able to defend and explain it.

AI in open source and the security-report flood

Patterson applies the same standard to contributors. He doesn't care whether a pull request was written with AI and doesn't ask for disclosure. Once a PR is merged, maintaining it becomes the maintainers' responsibility anyway. But he will ask questions about it, and if the author can't answer them, the PR probably won't land. He is fine with normal AI-assisted PRs: "You wanted a feature, you used… Claude to do it, seems good." He acknowledges that no policy will satisfy everyone, and points out that the line was already blurry when Copilot was just smart tab completion.

Security reports are harder. The Rails security team worked with the Internet Bug Bounty (IBB), which paid bounties to reporters of open-source vulnerabilities. Patterson says he was skeptical from the start because paying for reports attracts people looking for free money. Even before AI, there were low-quality reports and haggling over severity, since payouts scaled with severity. The reports he hated most claimed that writing control characters through the logger to a terminal "that hasn't been updated since 1996" could overwrite characters and hide log entries, and insisted this was the most critical bug imaginable. When AI arrived, a flood of low-effort reports followed. The IBB's response was to stop paying bounties, which Patterson says greatly reduced the number of reports. He doesn't consider that a good solution, because he wants security researchers to be paid. The team also uses AI to process reports. Robby notes that flooding a project's security address with realistic-looking reports is effectively a denial-of-service attack on the maintainers, and Patterson agrees. He calls the situation "damned if you do, damned if you don't" and says he has no good answer.

Both are wary of workflows where bots review bots' pull requests. Patterson predicts a new generation of AI-generated code patterns that will eventually get their own names, much like the design-pattern and anti-pattern vocabulary of the '90s and early 2000s ("big ball of mud"), and that we will need a way to tell a human-made ball of mud from an AI-made one.

What's exciting in Rails, and learning its internals

Patterson's favorite Rails work is low-level, and right now that means router performance. He has been looking into it and hopes to ship improvements that, like his other work, users get just by upgrading. On AI-oriented tooling, he mentions Ruby Dex as "very cool" though not Rails-specific, and says he loves tools that help people get into an application and start developing. He is a big fan of Marco Roth's work, especially Herb, and would like to see more of that built into Rails.

For developers who want to read Rails source, he suggests following their interests: Active Record for databases, Action View or Action Controller for views and controllers. When he started, he would pick something like link_to, ask how it worked, and read the code, hunting down where each method was defined. He says he is jealous of people starting today, because they can open Rails with Claude, ask how link_to works, and have it walk them through the code.

Book recommendations and where to find him

Patterson recommends Ruby Under a Microscope by Pat Shaughnessy, which he calls the best introduction to Ruby internals, covering introductory through advanced material. Shaughnessy is writing a second edition, which Patterson is technically reviewing. He thinks it may already be available as a preview and believes No Starch is publishing it, though he isn't sure. For anyone who wants to learn how JIT compilers or compilers in general work, he recommends Engineering a Compiler. He calls it very advanced and says it contains many of the techniques his team uses in their JIT.

He can be found on Bluesky as tenderlove and says he is trying to write more on his blog at tenderlovemaking.com. Several of the threads he raised were still in progress at the time of recording: making Rails Ractor-safe, the content-addressable gem proposal, which RubyGems.org seems receptive to but has not shipped, his planned router performance work, and his unfinished thinking about how far to trust code written by AI.