123456789_123456789_123456789_123456789_123456789_

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:

  1. Review the Upgrade section below to look for breaking changes that could affect you.
  2. Upgrade to version 8.0 in your Gemfile and deploy.
  3. Open up a new bug issue if you find any problems.
  4. 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:

  1. 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 advertise tcp://[::]:PORT whenever a non-loopback IPv6 interface is present. If you need the old IPv4 wildcard behavior, set bind 'tcp://0.0.0.0:9292', port ENV.fetch('PORT', 9292), '0.0.0.0', or set_default_host '0.0.0.0' explicitly, and review any firewall rules, health checks, deploy scripts, or host-string parsing code that assumed 0.0.0.0 or unbracketed HOST:PORT formatting.
  2. If you explicitly configure bind 'tcp://[::]:...', bind 'ssl://[::]:...', or equivalent ssl_bind '::', ..., Puma will now rewrite that unspecified IPv6 bind to 0.0.0.0 when 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.
  3. before_thread_start hooks now receive a ServerPluginControl argument. Update any zero-arity lambdas, method objects, or other strict-arity hook code to accept one argument, for example before_thread_start { |_control| ... }, or Puma can raise ArgumentError when the hook runs.
  4. On platforms without SIGINFO, Puma now traps SIGPWR, and pumactl info sends SIGPWR there. If your supervisor, init script, or container tooling already uses SIGPWR for something else, change that wiring before upgrading and update any runbooks that assumed Puma would ignore SIGPWR.
  5. Requests rejected by supported_http_methods are now treated as parser/client errors earlier in request processing. If you use supported_http_methods, re-test unsupported-method requests and do not depend on the old 501 timing, connection handling, or lowlevel_error_handler behavior; if your error handler still sees these requests, make sure it tolerates a less-normalized Rack env.
  6. http_content_length_limit enforcement is stricter now. A 413 on an HTTP/1.1 keep-alive request now forces connection: 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 a 413, and re-test any upload flows or custom error handling that depended on Puma's previous oversized-body behavior.
  7. If you use fork_worker, phased restart order changed. During USR1 or pumactl phased-restart, worker 0 is 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.