Rails Health Check
Beyond /up: Building Production-Grade Health Checks for Rails
Rails ships with a simple /up endpoint, which is perfect for answering one question:
"Is the application process alive?"
Unfortunately, that's often not the question we really need answered in production.
Modern Rails applications depend on far more than just the web process. A request may require:
- A database connection
- Redis
- Sidekiq or Solid Queue
- SMTP services
- External APIs
- Disk and memory availability
An application can happily respond with 200 OK while one of these critical dependencies has already failed.
That's the problem I wanted to solve when I built rails_health_checks.
The Problem with Basic Health Endpoints
Many applications start with something simple:
get "/up" => proc { [200, {}, ["OK"]] }This works well for liveness probes, but it doesn't tell us:
- Is PostgreSQL reachable?
- Has Redis gone down?
- Are background jobs backing up?
- Is the application running out of disk space?
- Is an external service unavailable?
Operations teams often end up stitching together custom controllers, ad-hoc checks, or older libraries that require significant configuration.
I wanted something that:
- Felt native to Rails.
- Was easy to configure.
- Supported modern queue adapters.
- Scaled well under heavy monitoring traffic.
- Produced structured responses and metrics.
Introducing rails_health_checks
rails_health_checks is a Rails engine that provides production-ready health endpoints with built-in checks, parallel execution, result caching, and Prometheus metrics.
It includes support for:
- Database
- Cache stores
- Redis
- SMTP
- Sidekiq
- Solid Queue
- GoodJob
- Resque
- Disk space
- Memory usage
- HTTP endpoints
Instead of writing custom health controllers, you can add comprehensive monitoring with only a few lines of configuration.
Installation
Add the gem:
gem "rails_health_checks"
Bundle and mount the engine:
mount RailsHealthChecks::Engine => "/health"
Generate the initializer:
rails generate rails_health_checks:initializer
A fully documented configuration file is created with sensible defaults.
Structured Health Responses
Requests to:
GET /health
return JSON like:
{
"status": "ok",
"checks": {
"database": {
"status": "ok",
"latency_ms": 4
},
"cache": {
"status": "ok",
"latency_ms": 1
}
}
}Overall status automatically reflects the state of individual checks:
okdegradedcritical
This makes the endpoint suitable for dashboards, monitoring systems, and incident diagnostics.
Parallel Execution
One issue with health checks is that they can become slow as more dependencies are added.
A naïve implementation runs checks sequentially:
- Database
- Redis
- SMTP
- Queue system
- External APIs
Latency becomes the sum of every check.
rails_health_checks executes checks in parallel using Concurrent::Future, so response time is bounded by the slowest dependency rather than the sum of all dependencies.
Benchmark results show five 10 ms checks completing in roughly 13 ms instead of over 60 ms, producing about a 4.5× speedup.
Result Caching
Monitoring systems often hit health endpoints every few seconds.
Without caching, each request is repeated:
- Opens database connections
- Talks to Redis
- Checks queue systems
- Calls external services
The gem can cache results:
RailsHealthChecks.configure do |config|
config.cache_duration = 10
end
This absorbs high-frequency probe traffic and dramatically reduces load on dependencies.
Benchmarking showed cache hits to be hundreds of times faster than re-running checks.
Check Groups
Not every consumer needs every check.
You may want:
config.group :system, [:disk, :memory]
config.group :workers, [:sidekiq, :good_job]
Which exposes:
/health/system
/health/workers
allowing infrastructure components to monitor only the dependencies they care about.
Prometheus Metrics
The gem also exposes:
GET /health/metrics
using Prometheus text exposition format.
This allows health status and latency information to be scraped directly by Prometheus and visualized in Grafana without additional code.
Custom Checks
Applications often have dependencies unique to their environment.
Creating a custom check is straightforward:
class PaymentGatewayCheck < RailsHealthChecks::Check
def call
response = Net::HTTP.get_response(
URI("https://api.example.com/status")
)
response.code == "200" ?
pass :
fail_with("API unavailable")
end
end
RailsHealthChecks.configure do |config|
config.register :payment_gateway,
PaymentGatewayCheck.new
end
This makes it easy to monitor internal APIs and third-party services alongside built-in checks.
Why I Built It
Over the years, I've seen many Rails applications outgrow simple liveness endpoints.
Teams eventually need:
- Better visibility into dependencies.
- Faster diagnostics.
- Monitoring-friendly responses.
- Prometheus integration.
- Support for modern job systems.
- Performance that doesn't become a bottleneck itself.
rails_health_checks aims to provide those capabilities while remaining simple enough to add to an existing Rails application in just a few minutes.
Conclusion
Health checks are often treated as an afterthought, but they become one of the most important tools when something goes wrong.
A good health endpoint should answer more than:
"Is the process running?"
It should answer:
"Is the application actually healthy?"
That's the goal behind rails_health_checks.