Puma: the thread flag is per worker, so the counts multiply
A Ruby/Rack web server built for parallelism
At a glance
- What is it?
- Puma is Ruby's multi-threaded and multi-process Rack server, and the configuration detail that bites hardest is that the thread setting applies to each worker separately. Its default thread pool differs on MRI, preloading and phased restart are mutually exclusive, and a missing OpenSSL header installs cleanly while silently removing SSL.
- Who is it for?
- Adopt Puma when you are running a Rack application on MRI and need both concurrency modes, because pre-forking with copy-on-write memory and a thread pool per worker is the combination it is built around, and it is the default server for Rails. Do not treat its headline thread numbers as totals, because the thread setting is per worker and the default pool is smaller on MRI than elsewhere.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- puma/puma7,928 ★ · +12
| Repository | 2026-09-22 | 2026-11-13 |
|---|---|---|
| puma/puma | 7.9k | 7.9k |
12 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Threads are per worker, so the two flags multiply
Puma has two independent knobs and the relationship between them is the first thing worth understanding.
The thread pool is set with a flag.
$ puma -t 8:32Worker processes are set with another.
$ puma -t 8:32 -w 3And then the README states the consequence in a single sentence: threads are still used in cluster mode, and the `-t` thread flag setting is per worker, so `-w 2 -t 16:16` will spawn 32 threads in total, with 16 in each worker process.
So the minimum and maximum you pass are per process, and the running total is that number multiplied by the worker count. With cluster mode enabled, each child also keeps its own pool, which is why a thread setting that looked conservative in single-process mode becomes substantial the moment you add workers.
The consequence is that a value tuned on a development machine with one worker can be an order of magnitude larger in production, and the contention it creates is not always obvious. On MRI specifically, the README warns that too many threads can cause contention for the Global VM Lock.
If you want the worker count to follow the machine, there is an automatic mode, either through a config file or an environment variable.
# config/puma.rb
workers :auto$ WEB_CONCURRENCY=3 puma -t 8:32The automatic mode needs the concurrent-ruby gem to be available in your application and resolves to the available processor count; with the environment variable it is WEB_CONCURRENCY set to "auto", while a number such as 3 fixes the worker count.
The default thread pool is smaller on MRI, and the floor is not one
The default pool is stated as two different numbers.
Puma will automatically scale the number of threads, from the minimum until it caps out at the maximum, based on how much traffic is present. The current default is `0:16` and on MRI is `0:5`.
So the format is minimum:maximum, the minimum is zero in both cases, and the maximum is sixteen by default but five on MRI. Which default you get depends on the Ruby you are running rather than on a setting.
The scaling behaviour is the other half. A minimum of zero means the pool grows with traffic rather than sitting at a fixed size, which is the sensible default for a server that might be idle between bursts.
Then there is a floor that is not in either number.
The README says that additionally Puma creates threads on its own for internal purposes, for example handling slow clients, so even if you specify -t 1:1, expect around 7 threads created in your application.
That is the detail to hold on to. A minimum and maximum of one does not limit the whole process to one thread; Puma documents additional internal threads, including for slow clients.
Account for Puma’s internal threads separately from request threads. Database connection and file descriptor usage depend on the application, pool configuration and runtime; do not equate every internal thread with a database connection.
Preloading and phased restart cannot both be on
Cluster mode can preload your application, and the mechanism is an operating system feature.
Preloading loads all the application code prior to forking, which reduces total memory usage via copy-on-write, since the workers share the master's pages until they write.
It is enabled by default once you have more than one worker, unless the prune-bundler flag has been specified. Otherwise you can ask for it from the command line.
$ puma -w 3 --preloadOr from a config file.
# config/puma.rb
workers 3
preload_app!Then the incompatibility, stated plainly.
Preloading can't be used with phased restart, since phased restart kills and restarts workers one-by-one, and preloading copies the code of master into the workers.
That is a genuine architectural conflict rather than a missing feature: a phased restart depends on workers running independently of the master's already-forked state, and preloading is precisely the act of copying the master's state into them.
The hooks carry the same dependency, with the README recommending you use them together with preloading, since constants loaded by your application, such as Rails, will not be available inside the hooks otherwise.
# config/puma.rb
before_fork do
# Add code to run inside the Puma master process before it forks a worker child.
end
before_worker_boot do
# Add code to run inside the Puma worker process after forking.
end
after_worker_shutdown do |worker_handle|
# Add code to run inside the Puma master process after a worker exits. `worker.process_status` can be used to get the
# `Process::Status` of the exited worker.
endSo if you want zero-downtime rolling restarts, you are choosing between preloading and phased restart.
Missing OpenSSL headers install cleanly and silently drop SSL
There is a failure mode in this README that is worth quoting in full because of what it does not say.
Puma will install/compile with support for ssl sockets, assuming OpenSSL development files are installed on the system.
If the system does not have OpenSSL development files installed, Puma will install/compile, but it will not allow ssl connections.
So the gem installs. Nothing errors. The absence of a build dependency does not fail the install, it just removes a capability, and you find out when you try to terminate a connection behind TLS.
There is a related capability claim in the feature list, that Puma is standalone with SSL support, zero-downtime rolling restarts and a built-in request bufferer, so you can deploy it without any reverse proxy. That claim depends on this build-time detection succeeding.
The repository does carry something for the problem. There is a certificate generation script in the examples directory with its own markdown file beside it, so there is at least a documented way to produce certificates for testing.
The consequence is that the deploy check should include a TLS termination attempt rather than a health check on plain HTTP. A container image built without the OpenSSL development packages will start, serve, and report itself healthy while being unable to do the one thing that made standalone deployment attractive.
The 7.x and 8.x lines ship within a minute of each other
The release record is worth reading carefully, because the newest tag is not the newest major.
The three most recent releases are v7.2.1 published on 2026-05-27 at 00:19:34, v8.0.2 published on 2026-05-27 at 00:18:49, and v8.0.1 published on 2026-04-26.
The two releases from 27 May are forty-five seconds apart. One is a patch on the 7.x line and the other is a patch on the 8.x line, published in the same minute.
Two things follow. The first is that 7.x is not abandoned, it is receiving maintenance releases alongside 8.x, so if you are on 7 you are not stranded. The second is that ordering by date does not put you on the newer major: v7.2.1 is the most recent release by timestamp and it is the older major version.
So any tooling that resolves latest by commit date rather than by major version will hand you the 7.x maintenance release, and a lockfile that floated during that window could have crossed a major boundary without anyone noticing.
The consequence is that when you pin, pin the line you intend. A version constraint on the 7.x series and one on the 8.x series are different decisions, and the release timestamps will not make that distinction for you.
rails server hides most of Puma, and the README says so
Puma is the default server for Rails and is included in the generated Gemfile, which means most Rails developers arrive at it without choosing it.
The framework section then warns against the obvious way to start it.
Start with the rails command.
$ rails serverMany configuration options and Puma features are not available when using `rails server`. It is recommended that you use Puma's executable instead.
$ bundle exec pumaSo the default entry point for a Rails application exposes a subset, and the project would rather you invoked the binary directly.
Sinatra has a similar wrinkle, in reverse. You can start a Sinatra application from the command line.
$ ruby app.rb -s PumaBut to configure Puma with a config file such as `puma.rb` you need the puma executable, and that requires a rackup file in the application.
# config.ru
require './app'
run Sinatra::Application$ bundle exec pumaAnd there is a diagnostic worth knowing about: setting the `PUMA_LOG_CONFIG` environment variable prints the loaded configuration during boot, which is the fastest way to find out what a config file actually did.
A custom cops directory and a todo file that admits the exceptions
The repository root shows how seriously the project takes its own linting, and the shape is unusual for a Ruby web server.
There are two RuboCop configuration files at the top level, the main one and a todo file, plus a `cops/` directory. A todo file is RuboCop's mechanism for listing offences that exist and are being tolerated, and shipping one in the repository is a way of saying which rules have been grandfathered rather than fixed.
The `cops/` directory goes further than configuration. By its name, it holds custom RuboCop cops.
Alongside that there is a Code Climate configuration file, a governance document, and a maintainers file with no file extension at all.
The release and history side is handled in two flat files, a history file and a release document.
There is also an `ext/` directory, which is where a native extension lives, and the README's claim that the HTTP parser is inherited from Mongrel and has over fifteen years of production use is the sort of thing that native code and an established parser lineage would support.
The consequence is that this is a repository where the tooling is part of the deliverable. If you are forking Puma, the cops directory tells you what the maintainers consider a mistake in this codebase, which is more information than a linter configuration alone.
What Puma cannot do, and what it leaves you owning
Five limits, and the first is the one the README is most candid about.
It cannot give MRI real parallelism. On MRI there is a Global VM Lock that ensures only one thread can run Ruby code at a time. The README's argument is that if you are doing a lot of blocking IO, such as HTTP calls to external APIs, Puma still improves throughput by allowing the IO waiting to happen in parallel, and it is explicit that truly parallel implementations like TruffleRuby and JRuby do not have this limitation.
So on the most common Ruby, Puma buys you concurrency over waiting, not concurrency over computation.
It cannot give you preloading and phased restart together. One copies the master's code into workers, the other replaces them one at a time, and the README states they are incompatible.
It cannot tell you the thread count you will end up with. The flag is per worker, the default differs by runtime, and Puma adds its own threads on top, so the running total is larger than any number in your configuration.
It cannot warn you that SSL is missing. A build without OpenSSL development files installs and runs and simply cannot terminate TLS.
And it cannot stand in for a reverse proxy for the features it does not implement. It is standalone in the sense that it can do SSL, rolling restarts and buffering itself, and that is a specific list rather than a claim to replace a proxy in every position.
Editorial conclusion
Adopt Puma when you are running a Rack application on MRI and need both concurrency modes, because pre-forking with copy-on-write memory and a thread pool per worker is the combination it is built around, and it is the default server for Rails. Do not treat its headline thread numbers as totals, because the thread setting is per worker and the default pool is smaller on MRI than elsewhere. Before you set a production value, check three things. Missing OpenSSL development files do not stop the gem installing, they just leave you without ssl, so verify that rather than assuming it. Preloading is on by default once you have more than one worker and cannot be combined with a phased restart. And the 7.x and 8.x lines are published within a minute of each other, so the newest tag by date is a maintenance release rather than the newest major, and pinning by version string needs care.
Frequently asked questions
What is Puma?
A simple, fast, multi-threaded and highly parallel HTTP 1.1 server for Ruby and Rack applications. It is both multi-threaded, serving each request in a separate thread, and multi-process, pre-forking workers in cluster mode. It is the default server for Ruby on Rails.
How do I install and start Puma?
With gem install puma, then run puma. Without arguments, Puma looks for a rackup file named config.ru in the working directory. For a Rails application the README recommends bundle exec puma over rails server, because many configuration options and features are unavailable through the Rails wrapper.
How many threads does Puma use by default?
The default pool is 0:16, and on MRI it is 0:5, with Puma scaling up from the minimum as traffic arrives. Additionally Puma creates threads of its own for internal purposes such as handling slow clients, so a setting of 1:1 produces roughly seven threads in your application.
Can Puma use preloading and phased restart together?
No. The README states that preloading cannot be used with phased restart, because phased restart kills and restarts workers one by one while preloading copies the master's code into the workers. Preloading is enabled by default when the worker count is greater than one unless prune-bundler is specified.
Why does the Puma thread count matter on MRI?
MRI has a Global VM Lock that lets only one thread run Ruby code at a time, so Puma's threads help with blocking IO rather than with computation. The README also warns that setting a large maximum thread count can exhaust system resources or cause contention for that lock, and that on JRuby and TruffleRuby the limitation does not apply.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/puma-puma)Star history chart for your README
[](https://hysenlabs.com/projects/puma-puma)Was this useful?