How Rack::Timeout Keeps Your App Alive

How Rack::Timeout Keeps Your App Alive
Photo by Linda Perez Johannessen / Unsplash

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