Welcome to Puma 8.0: Into the Arena.
Puma 8 brings IPv6 by default, increased control over the threadpool, and more.
Here's what you should do:
- Review the Upgrade section below to look for breaking changes that could affect you.
- Upgrade to version 8.0 in your Gemfile and deploy.
- Open up a new bug issue if you find any problems.
- Join us in building Puma! We welcome first-timers. See CONTRIBUTING.md.
For a complete list of changes, see History.md.
What's New
Smarter concurrency controls
IO-bound requests can now go past your normal thread ceiling. Puma 8 adds max_io_threads and injects env["puma.mark_as_io_bound"] into the Rack env so your app or middleware can tell Puma when a request has become mostly I/O wait. That helps mixed workloads a lot: slow API calls, report generation, and similar wait-heavy requests no longer need to crowd out CPU-bound work as aggressively. This landed in #3816 and was refined in #3894.
# config/puma.rb
threads 0, 5
max_io_threads 5
# config.ru
run lambda { |env|
env['puma.mark_as_io_bound'].call
report = SlowReportService.fetch
[200, { 'content-type' => 'application/json' }, [report]]
}
We anticipate this will mainly by used by framework authors who have threads or types of requests they know are extremely IO-bound, and don't recommend it for use at the application level.
Thread pool limits can be changed at runtime. Puma now exposes Puma::Server#update_thread_pool_min_max, and hook/plugin code can do the same through ServerPluginControl.
# from a plugin or other trusted in-process integration
server.update_thread_pool_min_max(min: 2, max: 12)
If you already use before_thread_start, note that hook behavior changed in this release; see the Upgrade section below.
Cleaner config for single and cluster mode
New single and cluster blocks let one config file express both modes cleanly. These blocks run after config files are loaded, so you can keep the mode-specific settings in one obvious place instead of scattering if logic through config/puma.rb. This makes shared configs much easier to read and reuse across development, review apps, and production. See #3621.
workers ENV.fetch('WEB_CONCURRENCY', 0)
single do
silence_fork_callback_warning
end
# Only runs if workers > 0
cluster do
preload_app!
before_worker_boot do
# Do a thing
end
end
Better debugging and operations
shutdown_debug can now be limited to forced shutdowns. If you want thread backtraces only when a graceful shutdown turns into a forced one, use shutdown_debug on_force: true. That keeps normal deploy logs quieter while still giving you the "what is hanging?" escape hatch when you need it. See #3671.
shutdown_debug on_force: true
force_shutdown_after 30
Thread backtrace via signal works on more platforms. On systems without SIGINFO, Puma now uses SIGPWR for thread backtrace dumps. See #3829.
Phased restart is safer with fork_worker. Puma no longer reforks from a stale worker 0 during phased restarts in fork_worker mode. There is nothing new to configure, but if you have tooling that watches worker order, check the Upgrade section because the rollout sequence changed. See #3853.
Networking and performance
Puma now prefers IPv6 wildcard binds when the host supports them. When Puma detects a non-loopback IPv6 interface, the default TCP host becomes :: and the default bind becomes tcp://[::]:PORT. That lines Puma up better with modern dual-stack hosts, while still falling back to IPv4 when IPv6 is unavailable. See #3847. If you need IPv4-only behavior, pin the bind explicitly.
bind 'tcp://0.0.0.0:9292'
There are also a couple of hot-path performance wins. JRuby gets a faster HTTP parser with fewer copies and cheaper lookups, and Puma now avoids redundant header key downcasing when building responses. See #3838 and #3874.
Upgrade
Check the following list to see if you're depending on any of these behaviors:
- If you rely on Puma's default bind from
bin/puma, Rack handler startup,port, or an implicit config file bind, Puma may now listen on::and advertisetcp://[::]:PORTwhenever a non-loopback IPv6 interface is present. If you need the old IPv4 wildcard behavior, setbind 'tcp://0.0.0.0:9292',port ENV.fetch('PORT', 9292), '0.0.0.0', orset_default_host '0.0.0.0'explicitly, and review any firewall rules, health checks, deploy scripts, or host-string parsing code that assumed0.0.0.0or unbracketedHOST:PORTformatting. - If you explicitly configure
bind 'tcp://[::]:...',bind 'ssl://[::]:...', or equivalentssl_bind '::', ..., Puma will now rewrite that unspecified IPv6 bind to0.0.0.0when the host has no non-loopback IPv6 interface, and it will warn on boot. If you were using::to force IPv6-only behavior or to detect IPv6 availability, switch to a concrete IPv6 address instead of::, or ensure the machine actually has a usable IPv6 interface before Puma starts. before_thread_starthooks now receive aServerPluginControlargument. Update any zero-arity lambdas, method objects, or other strict-arity hook code to accept one argument, for examplebefore_thread_start { |_control| ... }, or Puma can raiseArgumentErrorwhen the hook runs.- On platforms without
SIGINFO, Puma now trapsSIGPWR, andpumactl infosendsSIGPWRthere. If your supervisor, init script, or container tooling already usesSIGPWRfor something else, change that wiring before upgrading and update any runbooks that assumed Puma would ignoreSIGPWR. - Requests rejected by
supported_http_methodsare now treated as parser/client errors earlier in request processing. If you usesupported_http_methods, re-test unsupported-method requests and do not depend on the old 501 timing, connection handling, orlowlevel_error_handlerbehavior; if your error handler still sees these requests, make sure it tolerates a less-normalized Rackenv. http_content_length_limitenforcement is stricter now. A413on an HTTP/1.1 keep-alive request now forcesconnection: close, and chunked request bodies are rejected as soon as they cross the limit during parsing. Update clients, proxies, and tests not to reuse the socket after a413, and re-test any upload flows or custom error handling that depended on Puma's previous oversized-body behavior.- If you use
fork_worker, phased restart order changed. DuringUSR1orpumactl phased-restart, worker0is now reinserted and restarted/reforked first after replacement. Update deployment scripts, canary logic, or monitoring that assumed the old worker sequence or keyed rollout steps off worker index order.