#4014·puma

Feature: Add `after_request_complete` hook

Author: ybiquitousCreated Sep 7, 2026Updated Sep 7, 2026

Is your feature request related to a problem? Please describe.

I want a bounded worker lifetime in cluster mode: recycle a worker gracefully after it completes N requests. All I need from Puma for that is a callback that runs once per completed request inside the worker.

There's no such hook for now, so I do that in a Rack middleware instead, like this:

ruby
class PumaWorkerRecyclerMiddleware
  def call(env)
    status, headers, body = @app.call(env)
    [status, headers, BodyWrapper.new(body) { record_request }]
  end

  private def record_request
    requests_count = nil
    @mutex.synchronize do
      @count += 1
      requests_count = @count
    end
    if requests_count > @max_requests_count
      Process.kill('TERM', Process.pid) # retire a worker process
    end
  end
end

My actual code has more lines, though.

Describe the solution you'd like

ruby
after_request_complete do |server|
  # worker process, request thread, after the response body has been written
  # server.requests_count is the per-worker completed-request count
end
  • Runs after every completed request, whatever the thread count and whether or not other threads are busy.
  • Has to be fast. Unlike the out_of_band hook, it should not hold off the accept loop.
  • If the new hook raises, Puma rescues and logs it, the same way trigger_out_of_band_hook does.
  • Runs inline on the request thread, so Puma doesn't need a new thread for it.

This isn't asking Puma to own any policy. No thresholds, no defaults, nothing that reads memory, nothing that kills a worker. My middleware would go away and config/puma.rb would get one line like this:

ruby
after_request_complete do |server|
  PumaWorkerRecycler.check(server.requests_count)
end

Describe alternatives you've considered

out_of_band is the closest thing that exists, but it doesn't fit. See the doc and implementation below:

Both of those are right for out-of-band GC, which is what the hook is for, so I'm not asking to change it.

Additional context

Pitchfork has this hook under the same name, after_request_complete, described as:

Can be used for out of band work, or to exit unhealthy workers.

Same stance as Puma on not shipping a killer, but with the hook available, the implementation is about five lines.

Also, there were similar requests, but rejected:

  • #1656
  • #2659

I'm not trying to reopen either. Policy can stay outside Puma.

Puma already recycles workers based on a request count in fork_worker, so a request-count-driven worker lifecycle isn't foreign here. The name follows the before_* / after_* convention from the v7.0 rename.

I'd be happy to discuss the hook idea if you're open to it. Thank you.