ruby 103 lines · 3 tabs

Health Check Endpoint with Dependency Probes

Shared by codesnips Jan 2026
3 tabs
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
3 files · ruby Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Health Check Endpoint with Dependency Probes — share card
Link copied