Baking with Rails at scale: recipes in Ruby, cookware from Go, C, and Rust

Ruby on Rails excels at business logic, developer happiness, and rapid iteration. Still, when performance bottlenecks emerge, the typical refrain is: “rewrite in Go,” “extract to microservices,” or “break up the monolith.” But there’s a better way: scaling your Rails app without breaking the recipe by optimizing your kitchen instead.
TL;DR: Keep your cake recipe (business logic) in Ruby, but upgrade the tools in your kitchen with Go, C, Rust (and so on) for fast mixing and baking. Never sacrifice flavor for speed!
Hire Evil Martians
Let's explore whether strategic extraction could help your team scale without losing the productivity that made Rails valuable in the first place.
The art of surgical extraction
A Rails application is a bakery with a living recipe, namely your business logic, which is constantly refined through feedback and innovation. However, when orders begin to flood in, the kitchen starts to groan. The temptation is to rewrite the recipe in a “faster” language. But recipes aren’t the bottleneck, it’s the mechanical work overwhelming your bakers!
The solution is upgrading your mixers and ovens, not your recipes: identify the repetitive, CPU-intensive tasks and delegate them to specialized tools built in Go, C, Rust, and so forth. These helpers don’t need to know anything about your secret ingredients or special techniques, but they’re going to excel at the heavy lifting.
This approach preserves the ability to innovate on recipes while performant tools handle the grunt work. Meanwhile, your signature flavor can remain agile, alive, and never stale.
Recipe #1: imgproxy, or when the kitchen needs faster image variants
Picture your bakery suddenly swamped with photo orders: gallery displays crawling, image uploads timing out, background jobs backing up. Your hand mixers (like Active Storage’s .variant
calls) can’t keep up, consuming massive resources and blocking other kitchen work.
Then, you discover imgproxy, the fastest mixer of image variants, built in Go. But how do you integrate it without retraining your bakers or rewriting recipes?
Here’s the architectural insight: in a case like this, Rails already has a system for delegating work to “a better mixer”. When you call image.variant(resize_to_limit: [300, 300])
, Rails creates a variant object that generates a signed URL pointing to ActiveStorage::Representations::*Controller
. This controller acts as a dispatcher: it checks if the variant exists, processes it if needed (using the configured processor), and serves or redirects to the result.
Rails uses libvips as the default variant processor, and it can also use ImageMagick. But this entire flow (URL generation, processing delegation, and serving the result) is designed as an adapter pattern. We just need to route those transformation instructions to imgproxy instead of the in-process handlers.
Note that the recipe stays pure (checking permissions, validating dimensions, making business decisions) and the mixer never sees user contexts or business rules—it just follows the transformation instructions.
Plot twist: we’ve already built this kitchen upgrade! The imgproxy-rails gem swaps your mixers with ease; drop it in, connect via configuration, and every recipe will automatically use the new equipment.
# Gemfile
gem 'imgproxy-rails'
# config/production.rb (or wherever you want imgproxy active)
config.active_storage.resolve_model_to_route = :imgproxy_active_storage
# config/development.rb (keep using Rails processing locally)
config.active_storage.resolve_model_to_route = :rails_storage_proxy
Your bakers never know the difference: same ingredients, same techniques, just faster results. The same view code now delegates the heavy image processing from Ruby to Go.
Three principles for power-ups that don’t suck
The approach of swapping parts of Rails with services in other languages is widely used, but after working with dozens (if not hundreds) of growing Rails startups, we’ve learned the three design principles that determine the longer-term success of these improvements. Here they are:
Rule #1: Extend Rails natively
Great performance helpers plug into Rails’ existing abstractions, rather than fighting them.
Rails provides dozens of extension points, and while Active Record database adapters are naturally the most commonly used, there are many more. Active Storage, Cache Store, Session Store, Action Cable, Action Mailer, Active Job are all great examples of this. They all provide a way to extend Rails’ existing abstractions rather than fighting them.
Vladimir Dementyev’s Layered design book teaches you to use and understand existing patterns and build new ones as your application grows.
Rule #2: Tools do not need to know the recipes
Performance helpers should be pure execution engines, completely unaware of your business recipes. The moment they start making decisions about users, permissions, UX, or domain logic, you’ve crossed from performance helper into microservices territory. Our high-performance helpers follow instructions; they never write the menu.
Rule #3: Plan out maintenance
Building and maintaining performance helpers in foreign languages requires team expertise you might not have. Therefore, whenever possible, lean on battle-tested open source solutions that handle the operational complexity for you. Custom solutions should be your last resort, not your first instinct.
TL;DR: The best performance upgrades feel invisible to your team and native to Rails. They scale the mechanical work without touching the creative process that makes your product unique.
But why? Adding a pinch of philosophy to our dish
Our recommendations are rooted in practice, taken from nearly two decades of resolving performance, scalability, and maintainability issues for web applications that were primarily built with Rails.
But we can back these guidelines up with little theory, too! Software design is a matter of making tradeoffs, and the two biggest are performance and developer productivity (also known as “time to market”). We can either make a piece of software extremely efficient and performant, or we can make it easy to manipulate and iterate on.
In the world of product development, losing developer productivity for the sake of performance is a luxury we can’t afford. Product development is all about iterations, agility, and changing things up in new and unexpected ways. The elegance of Ruby and the conventions of Rails are the best fit for this job.
At the same time, if we can separate a “performance-focused” tool that’s built in a performance-centric stack so that it’s not coupled with our product development process, we can achieve the best of both worlds.
You can call this tool “dumb”, “unaware”, or you can call it “universal”. In any case, it allows us to keep the creative process of product iterations, while ensuring optimal performance of the application.
Recipe #2: Playbook’s cloud processing
When working on a project for Playbook.com, instead of processing files inside Rails (and hogging web workers/memory), we decided to delegate the computationally expensive tasks: metadata parsing, generating thumbnails, AI recognition, to Google Cloud services, with reliability and scalability handled by GCP’s infrastructure.
Once a file upload lands in Google Cloud Storage, a serverless function kicks off the asynchronous processing pipeline:
- The cloud function spins up a Cloud Tasks job, retries and all.
- Each step is handled independently: image recognition, resizing, NSFW checks, you name it.
- The results are written to Cloud Firestore.
- Rails picks up processed data via a scheduled Sidekiq worker.
Pattern: keep Rails for servicing user-facing business logic. All the heavy lifting happens out-of-process, powered by Google’s serverless backbone.
Outcome: scalable, fast, and resilient file pipelines, without bogging down your Ruby web workers.
AnyCable follows the same pattern: swap the old oven (Action Cable written in Ruby) for a turbocharged one built in Go. Your recipes for real-time communication stay unchanged meaning more capacity with the same developer joy.
Here’s another example of taking this further by extending AnyCable’s capabilities but still keeping it unaware of the business logic. The mixers handled the mechanical work; the chef retained full control of the recipe.
Kitchen upgrade examples
Here are some examples of kitchen upgrades that follow this pattern:
-
Naturally, let’s start with the web server. Puma implements
Rack::Handler
, and Falcon, a Fiber-based async server with HTTP/2 support, is an alternative. -
ActiveStorage provides a range of adapters, including:
ActiveStorage::Service
allows us to extend the classic S3, GCP, and Azure, with alternatives like Hetzner, Cloudflare R2, DigitalOcean Spaces, and more -
ActiveStorage::Variant
can be used for on-demand file transformation tools like the aforementioned imgproxy.ActiveStorage::Analyzer
for metadata extraction (Poppler).ActiveStorage::Previewer
to generate custom previews (MuPDF). -
Background job solutions:
ActiveJob::QueueAdapters::*Adapter
allows us to integrate with alternative job queues including Sidekiq, GoodJob, Karafka, Temporal, Faktory, and more. -
Mail delivery methods:
ActionMailer::Base.delivery_method
allows us to extend the classic SMTP, sendmail, file, with alternatives like Postmark, SendGrid, Amazon SES, Mailgun, and more. -
WebSockets adapters: We’re leading the charge on the server adapterization of Action Cable to support more platforms and technologies, together with Samuel Williams. Our goal is to provide a seamless integration with various WebSocket servers and frameworks, including Async Cable, AnyCable, and others.
The Rails bakery advantage
Modern Rails bakeries, designed thoughtfully, serve massive crowds without losing their soul. Ruby engines get faster with YJIT improvements, while async features and the Ruby Fiber Scheduler keep the ovens running in parallel without burning out the staff. Performance-first kitchen helpers resolve bottlenecks, smart caching prevents waste, and your recipes continue evolving in Ruby—agile, alive, responsive to customer feedback.
The recipes drive innovation, while at the same time, specialized tools (written in C, Go, Rust, and more) scale the mechanical work. Together, they create bakeries that are fast, maintainable, and endlessly creative!
Scale your Rails bakery by upgrading the equipment, never by rewriting the recipes. Keep the business logic fresh in Ruby and upgrade your kitchen with industrial-grade tools that do the heavy lifting. Combine them, and bon appétit!