require "timeout"
module HealthCheck
class Probe
Result = Struct.new(:name, :status, :latency_ms, :critical, :error, keyword_init: true) do
def healthy?
status == :ok
end
def to_h
h = { status: status.to_s, latency_ms: latency_ms }
h[:error] = error if error
h
end
end
@registry = {}
class << self
attr_reader :registry
def register(name, critical: false, timeout: 2.0, &block)
registry[name.to_sym] = new(name, critical: critical, timeout: timeout, &block)
end
def all
registry.values
end
end
def initialize(name, critical:, timeout:, &block)
@name = name.to_sym
@critical = critical
@timeout = timeout
@block = block
end
attr_reader :name, :critical
def run
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
Timeout.timeout(@timeout) { @block.call }
Result.new(name: name, status: :ok, latency_ms: elapsed(started), critical: @critical)
rescue Timeout::Error
Result.new(name: name, status: :timeout, latency_ms: elapsed(started), critical: @critical, error: "exceeded #{@timeout}s")
rescue => e
Result.new(name: name, status: :error, latency_ms: elapsed(started), critical: @critical, error: e.message)
end
private
def elapsed(started)
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(1)
end
end
end
HealthCheck::Probe.register(:database, critical: true, timeout: 2.0) do
ActiveRecord::Base.connection.execute("SELECT 1")
end
HealthCheck::Probe.register(:redis, critical: false, timeout: 1.5) do
pong = Rails.cache.redis.with { |c| c.ping }
raise "unexpected ping response: #{pong}" unless pong == "PONG"
end
HealthCheck::Probe.register(:sidekiq, critical: false, timeout: 1.5) do
latency = Sidekiq::Queue.new("default").latency
raise "queue latency #{latency.round}s too high" if latency > 30
end
require "concurrent"
class HealthController < ActionController::API
# Liveness: process is alive, no dependencies touched.
def live
render json: { status: "ok" }, status: :ok
end
# Readiness: every dependency probed concurrently.
def ready
results = probe_all
status = overall_status(results)
render json: {
status: status,
revision: ENV["GIT_SHA"],
checks: results.each_with_object({}) { |r, h| h[r.name] = r.to_h }
}, status: status == "down" ? :service_unavailable : :ok
end
private
def probe_all
HealthCheck::Probe.all
.map { |probe| Concurrent::Promises.future { probe.run } }
.map(&:value!)
end
def overall_status(results)
return "down" if results.any? { |r| r.critical && !r.healthy? }
return "degraded" if results.any? { |r| !r.healthy? }
"ok"
end
end
A health check endpoint is more than a 200 OK — for orchestrators like Kubernetes it must distinguish liveness (the process is up) from readiness (the process can actually serve traffic because its dependencies are reachable). This snippet models that distinction as a small registry of probes plus a controller that runs them concurrently.
In HealthCheck::Probe, each dependency is wrapped in a lambda registered via HealthCheck::Probe.register. The run method wraps the block in a Timeout.timeout so a hung TCP connection to Redis or a slow SELECT 1 never blocks the whole check past timeout. Any exception is caught and folded into a structured Result struct carrying status, latency_ms, and an optional error message, so a single failing dependency degrades the report instead of raising. The critical flag distinguishes dependencies that must be up (the primary database) from optional ones (a cache) — this is what lets readiness fail selectively.
config/initializers/health_probes.rb is where the actual dependencies are declared. It probes the database with ActiveRecord::Base.connection.execute("SELECT 1"), Redis with a ping, and Sidekiq's queue latency, marking only the database as critical: true. Keeping registration in an initializer means the set of probes is defined once at boot and is trivial to extend without touching the controller.
HealthController exposes three routes. live is deliberately trivial — it returns immediately without touching any dependency, because a liveness probe should only report whether the process itself is wedged; making it depend on Postgres would cause Kubernetes to kill healthy pods during a database blip. ready runs every probe through a concurrent-ruby Concurrent::Promises.future fan-out so the total latency is roughly the slowest probe rather than their sum, then computes the HTTP status from critical failures via overall_status. A non-critical failure still returns 200 but surfaces status: "degraded" in the JSON body for dashboards.
The main trade-offs: Timeout.timeout can interrupt at awkward points, so probes stay to cheap, side-effect-free reads. Running probes concurrently bounds tail latency but multiplies connection usage, so the pool must be sized accordingly. This pattern scales cleanly — adding a new dependency is one register call.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
Share this code
Here's the card — post it anywhere.