The Ruby app server ecosystem has consolidated around three app servers: Unicorn, Puma, and Passenger 5. When creating a new Engine Yard environment, have to select between these three different application servers.
How do you pick the right app server? In this article, we'll cover all of the above, as well as compare these three leading Ruby app servers.
How important is an app server's raw speed?
An app server's 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.
If raw speed isn't critical, what is?
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 seconds is running: just impact the poor user that triggered the query.
Ruby's ecosystem of app servers has 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.
Puma and Passenger are equipped to handle slow clients. Unicorn cannot help with slow clients by itself: requests go directly to a worker process. The 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 this by using Nginx as a reverse proxy and letting it buffer client requests.
While clustering helps with slow I/O, it's not the most efficient approach:
- A single Rails process uses a considerable amount of memory - frequently 100 MB+ - but each extra thread inside that process only takes up a fraction of a megabyte.
- Most app servers are memory-constrained and more processes only push you closer to that limit.
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 the 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.
Multiple Processes + Multithreading help eliminate hiccups from slow Ruby code and I/O
In summary, a single host serving a Ruby app needs two things to provide a consistent experience:
- Multiple processes: your host can run Ruby code across multiple requests concurrently.
- Multithreading: your host can more efficiently use memory when waiting on I/O.
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 maybe prematurely bottlenecked by slow I/O.
- Puma and Passenger Enterprise can serve multiple processes (clustering) and perform multithreading.
- Unicorn and Passenger open source only supports clustering.
Which app server should I use?
Due to Unicorn's lack of multithreading and the dependency on Nginx for slow client request buffering, the argument for using Unicorn becomes harder nowadays. Its 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?
Passenger goes beyond Ruby: it runs Python, Node.js, and Meteor apps as well. Passenger is a good fit when you have apps developed in several languages that Passenger supports and you want to consolidate your app server stack, or when you don't mind paying for an app server to offset risk. Passenger Enterprise offers a compelling risk-averse choice vs. Puma: more extensive documentation, many configuration options, debugging tools, and dedicated support.
Passenger is a less clear fit when you are serving only Ruby apps, you don't want to pay for a server or you don't want to pay for an app server and are operating in a memory-constrained environment (like Heroku) and can't add I/O concurrency via multithreading.
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 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.
Bear in mind that v7 Stack was tailored to work well with Puma. This means that, if you try to implement Puma in versions before v7, you have a higher chance to have weird or wrong behaviors from this app server.
Unicorn, Passenger 5, and Puma Feature Comparison
Here's a table comparing each of the app servers we've covered in several important areas:
|Slow client buffering||No||Yes||Yes|
|Support||Open Source||Open Source||Open Source / Paid|
|Installation||Gem||Gem||Binary or Gem|