That is impossible to answer 100% accurately without a proper A-B test, last thing I want to do is bad-mouth Puma, I also have a great relationship with the Puma maintainers.
What our unicorn setup gives us excellent predictability regarding performance. A unicorn worker can only serve one active (non message bus / non hijacked) request at a time. That means quite a few things in the Ruby world.
-
Due to global interpreter lock, if you start serving more than 1 request per worker you start bottlenecking on the lock making stuff slower.
-
If you run lots of requests on a single worker concurrently memory usage can spike and you can bloat Ruby heaps
-
Only clean way of timing out a request is tearing down a process, unicorn makes this very straight forward and the forking model makes the cost of timing out low.
Unicorn has served us enormously well, and I see no plans to experiment with anything else. That said if memory is super tight I can see an advantage to using puma but the behavior of “low memory” + “misbahving app” on a single process puma could be very very painful.