Adding Graceful Shutdown to a Crystal HTTP Server
Learn how to add graceful shutdown handling to a Crystal HTTP server so it stops accepting new connections, finishes existing requests, and exits cleanly on SIGINT/SIGTERM.
06 May 2026, 08:15 UTC

Desired Outcome
When a SIGINT or SIGTERM signal is received, the Crystal HTTP server stops accepting new connections, allows any in‑flight requests to finish, and then exits with status 0.
Prerequisites
- Crystal 1.0+ installed (
crystal --versionshows ≥1.0.0). - A basic HTTP server file (e.g.,
server.cr) that requires'http/server'and starts a server. - Familiarity with spawning fibers and using
Signal.trap.
Implementation
Store the server instance in a top‑level variable so it can be accessed from the signal handler.
require 'http/server' server = HTTP::Server.new(8080) do |context| context.response.content_type = 'text/plain' context.response.print 'Hello from Crystal!' endSpawn a fiber that waits for a termination signal. Using
Signal.trapinside this fiber ensures the trap is set only once.spawn do Signal.trap(SIGINT) do puts \"Received SIGINT – initiating graceful shutdown\" server.close # stops accepting new connections end Signal.trap(SIGTERM) do puts \"Received SIGTERM – initiating graceful shutdown\" server.close end # Keep the fiber alive until the process ends sleep endStart the server in a fiber so the main fiber can stay alive to receive signals.
spawn do puts \"Server listening on http://0.0.0.0:8080\" server.listen endBlock the main fiber (e.g., with
sleep) so the program does not exit immediately.sleep # main fiber waits here(Optional) Track active requests with a counter to ensure the process exits only after all requests finish.
active = 0 counter = Channel(Int32).new server = HTTP::Server.new(8080) do |context| counter.send(1) # increment active += 1 context.response.content_type = 'text/plain' context.response.print 'Hello from Crystal!' # simulate work sleep 0.5 counter.send(-1) # decrement active -= 1 end # In the signal trap, after server.close, wait for counter to zero spawn do Signal.trap(SIGINT) do puts \"SIGINT: closing server\" server.close # drain counter loop do break if counter.receive == 0 end puts \"All requests finished – exiting\" exit 0 end # same for SIGTERM end
Expected Checks
- Run the server:
crystal run server.cr. - Send a few concurrent requests, e.g.,
for i in {1..5}; do curl -s http://localhost:8080 & done. - Press Ctrl‑C (SIGINT) or send
kill -TERM <pid>. - Observe that:
- New connection attempts are immediately refused (connection reset).
- In‑flight requests complete and return the expected response.
- The process exits with status 0 (
echo $?prints 0). - If you added the optional counter, verify that the program only exits after the counter reaches zero (no stray fibers).
Recovery / Rollback Options
If the graceful shutdown hangs (e.g., a handler blocks indefinitely), you can:
- Force‑shutdown after a timeout: in a supervisor script, wait
Nseconds after sending SIGTERM, then runProcess.kill(pid, SIGKILL). - Rollback code changes: revert the added
Signal.trapblock and theserver.closecall, restoring the previous version ofserver.cr.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.