July 08, 2019
Updated version of an article first published on February 10th, 2017.
The Ruby app server ecosystem has consolidated around three app servers: Unicorn, Puma, and Passenger 5. What specific problems must an app server solve for Ruby? How do you pick the right app server? Is there a need for each of these app servers in 2019?
In this post I'll cover all of the above, as well as compare and contrast the three leading Ruby app servers.
An app server's raw speed is unlikely to be a factor for the vast majority of apps. The execution time of your application code, database queries, and HTTP calls likely dwarfs the microsecond or millisecond difference in response times among Ruby's app servers. Unicorn, Puma, and Passenger are plenty fast for almost every Ruby app.
I wouldn't give much weight to benchmarked performance metrics, especially those that hammer an app server with hundreds of concurrent requests without throttling (ie
siege -b <URL>). This is far from a realistic request pattern for almost every web app.
A web app is a complex system of software, hardware, and networking. While these work most of the time, every high-throughput production app I've worked on has daily hiccups, like a 30 second database query or a 60 second external HTTP API call. It's important that these incidents are as isolated as possible. For example, don't impact every user of your app while an outlier 30 second is running: just impact the poor user that triggered the query.
Ruby's ecosystem of app servers have evolved to help absorb the impact of three recurring incidents that could have widespread impacts on your app experience. Let's look at those problems and how app servers help prevent them.
Billy just took a sweet photo of a moose while driving through the mountains. He needs to upload the photo ASAP to your app while he has some great hashtags ready-to-go. Unfortunately, cell reception in the mountains is spotty and Billy is stuck with a poor 3G connection. His photo upload would take just a couple of seconds on a 4G connection but is slowing to a two-minute crawl in the mountains.
How does your app server handle slow clients? Will processing a slow client request block the app server from conducting other work?
In the Ruby app server ecosystem, both Puma and Passenger handle slow clients in a similar fashion: request buffering. A separate process downloads incoming requests. Only when the request has completed is it passed on to an available worker.
Puma and Passenger are equipped to handle slow clients. Unicorn cannot help with slow clients by itself: requests go directly to a worker process. Unicorn doesn't hide this. The Unicorn docs clearly state: "You should not allow unicorn to serve clients outside of your local network". However, you can get around by using Nginx as a reverse proxy and letting it buffer client requests.
Your app is chugging through some complex image processing logic. What happens to requests that try to reach your app while your app server is cranking away on your Ruby code?
If your app is running Ruby MRI (and that's most apps), your app is subject to the global interpreter lock. This means only one thread in a Ruby process can execute your application code at any time. To process more requests concurrently, your app server must support clustering: it must spawn multiple processes to execute application code on the same host across multiple requests.
Puma, Passenger, and Unicorn all have clustering support.
Oops - you forgot to add a
limit(10) to a query that was just triggered by your largest customer with 10k database rows. Are other requests blocked while the database query executes, eventhough your app server isn't doing any work?
While clustering helps with slow I/O, it's not the most efficient approach:
The most efficient way to tackle slow I/O is multithreading. A worker process spawns several worker threads inside of it. Each request is handled by one of those threads, but when it pauses for I/O - like waiting on a db query - another thread starts its work. This rapid back & forth makes best use of your RAM limitations, and keeps your CPU busy.
Puma is the only open-source option for multithreading. Passenger Enterprise provides multithreading support. Unicorn does not support multithreading.
In summary: a single host serving a Ruby app needs two things to provide a consistent experience:
Without multiple processes, your app is bottlenecked by Ruby code execution as only one process thread can run Ruby code at a time. Without multithreading, your app may be prematurely bottlenecked by slow I/O.
Puma and Passenger Enteprise can serve multiple processes (clustering) and perform multithreading. Unicorn and Passenger open source only supports clustering.
Ruby apps have survived for years with just clustering: Puma was created in 2011 and Ruby apps obviously functioned before 2011. So yes, you can get by with just clustering. It's a question of efficiency: it costs a lot more to achieve concurrency scaling with processes versus threads.
Also, if your app isn't I/O bound, a multithreaded server won't make much of a difference. This, however, is not common for the majority of web apps that reach out to databases and other services and spend significant time waiting.
I'm probably stating the obvious, but you can't use a multithreading app server if your app isn't threadsafe. In that case, Puma, Unicorn, and Passenger can all work. For Puma, you'll just limit each worker process to one thread. It then behaves like Unicorn or the open-source Passenger version.
Due to Unicorn's lack of multithreading and the dependency on Nginx for slow client request buffering, the argument for Unicorn is hard in 2019. It's best fit is for internally-facing, non-threadsafe apps that aren't subject to slow clients. That said, I don't believe there would be a noticeable difference using Passenger or Puma in this case. Why learn the ins-and-outs of another app server?
The folks at Phusion have rightfully built up solid cred in the Ruby community over the years. They've been digging deep into the internals of Ruby since their release of Ruby Enterprise Edition in 2008. Their documentation is extensive. Passenger also goes beyond Ruby: it runs Python, Node.js, and Meteor apps as well.
Passenger is a good fit when:
Passenger is a less clear fit when:
There's a reason Puma is the default app server for newly generated Rails apps and on Heroku today: it's easy to configure and mostly "just works" out-of-the-box. It makes a lot of sense to start with Puma and evaluate Passenger as your app grows and needs more advanced features and configuration options.
Here's a table comparing each of the app servers we've covered in a several important areas:
|Slow client buffering||No||Yes||Yes|
|Support||Open Source||Open Source||Open Source / Paid|
|Installation||Gem||Gem||Binary or Gem|
Adding servers can be a band-aid for slow code. Scout APM helps you find and fix your inefficient and costly code. We automatically identify N+1 SQL calls, memory bloat, and other code-related issues so you can spend less time debugging and more time programming.
Ready to optimize your site? Sign up for a free trial.
Want more Rails insights like this delivered monthly to your inbox? Just put your email into the sidebar form.
Ready to optimize your site? Sign up for a free trial.