How Rack::Timeout Keeps Your App Alive
Rack::Timeout is a library that monitors long-running requests in a Ruby app and kills them if they take too long. The default configuration in Rack::Timeout is to kill requests that take 15 seconds or longer.
Why Do Requests Take A Long Time?
The answer here will vary from one app to another. A frequent culprit is database transactions taking too long or outgoing requests to other resources hanging.
How Does This Keep My App Alive if it Kills Things?
In a typical Ruby app using Puma, you will have a few processes to manage a Ruby Thread collection. Rack::Timeout will send SIGTERM to a process if it breaches the timeout.
Your Ruby app and Puma process will remain alive, but the stuck process will be terminated.
Puma will notice that this process has terminated and will boot up a new one.
When is Rack::Timeout Unsafe?
Killing a running process is often unsafe. Fortunately, Rack::Timeout uses a "polite" kill message, SIGTERM, which requests that the process shut down and clean up its resources.
SIGTERM is less severe than SIGKILL, but still poses risks.
A Ruby thread could be in the middle of creating database records when it is killed, which would result in things being left in a broken state.
Where Does Rack::Timeout Run in My App?
Rack::Timeout runs in Rack, an interface between web servers, like Puma, and Rails applications. Puma receives a request, hands it off to a thread, which passes it through Rack before it reaches your application code.
When Rack handles the request, it monitors the request duration and kills the process if the request takes too long.
Rack::Timeout keeps your application alive by killing hanging processes and allowing Puma to boot up a new process to handle the next request.
graph TD Client[Client Requests] --> LB[Load Balancer nginx/HAProxy] LB --> Container1[Container 1 Rails App] LB --> Container2[Container 2 Rails App] Container1 --> PumaServer1[Puma Server 1] Container2 --> PumaServer2[Puma Server 2] PumaServer1 --> Worker1_1[Worker 1] PumaServer1 --> Worker1_2[Worker 2] PumaServer2 --> Worker2_1[Worker 1] PumaServer2 --> Worker2_2[Worker 2] Worker1_1 --> RackTimeout1_1[Rack::Timeout Middleware] Worker1_2 --> RackTimeout1_2[Rack::Timeout Middleware] Worker2_1 --> RackTimeout2_1[Rack::Timeout Middleware] Worker2_2 --> RackTimeout2_2[Rack::Timeout Middleware] RackTimeout1_1 --> Threads1_1[3 Threads Thread Pool _timeout monitored per thread_] RackTimeout1_2 --> Threads1_2[3 Threads Thread Pool _timeout monitored per thread_] RackTimeout2_1 --> Threads2_1[3 Threads Thread Pool _timeout monitored per thread_] RackTimeout2_2 --> Threads2_2[3 Threads Thread Pool _timeout monitored per thread_] classDef container fill:#e1f5fe classDef worker fill:#f3e5f5 classDef thread fill:#e8f5e8 classDef loadbalancer fill:#fce4ec classDef middleware fill:#fff3e0 class Container1,Container2 container class Worker1_1,Worker1_2,Worker2_1,Worker2_2 worker class Threads1_1,Threads1_2,Threads2_1,Threads2_2 thread class LB loadbalancer class RackTimeout1_1,RackTimeout1_2,RackTimeout2_1,RackTimeout2_2 middleware