Rails 7.1 shipped with something developers had been cargo-culting for years: a built-in /up health check endpoint. It's a small addition with a big implication — health monitoring is now a first-class concern in Rails, not an afterthought.
But a basic health endpoint is just the starting point. Production Rails APIs also need visibility into Sidekiq background jobs, database connection pools, Redis, and external service dependencies. This guide walks through the complete monitoring setup: from Rails' built-in /up endpoint to a full Vigilmon integration with Slack alerting.
The Rails 7.1+ /up Endpoint
Rails 7.1 added a HealthController and routes it to /up automatically:
# config/routes.rb (Rails 7.1+)
Rails.application.routes.draw do
# This is added automatically when you create a new Rails 7.1 app
# Equivalent to:
get "/up", to: "rails/health#show", as: :rails_health_check
end
The default controller returns 200 OK if Rails can connect to all configured databases and 500 otherwise:
curl https://api.yourdomain.com/up
# HTTP/2 200 OK
# x-runtime: 0.003
This is immediately monitorable. Set up a Vigilmon HTTP monitor on /up and you'll know within a minute if your Rails server or database connection is down.
However, the default /up only checks ActiveRecord connectivity. It won't catch a dead Redis connection, a Sidekiq queue backlog, or a broken third-party API integration. For comprehensive coverage, you'll want a custom health endpoint.
Custom Health Controller
Build a health controller that checks all critical dependencies:
# app/controllers/health_controller.rb
class HealthController < ApplicationController
skip_before_action :authenticate_user!, raise: false
def show
checks = {}
overall_status = :ok
# Database check
checks[:database] = check_database
checks[:redis] = check_redis
checks[:sidekiq] = check_sidekiq
checks[:storage] = check_storage
if checks.values.any? { |c| c[:status] == 'error' }
overall_status = :service_unavailable
end
render json: {
status: overall_status == :ok ? 'ok' : 'degraded',
checks: checks,
rails_env: Rails.env,
timestamp: Time.current.iso8601
}, status: overall_status
end
private
def check_database
ActiveRecord::Base.connection.execute('SELECT 1')
{ status: 'ok', latency_ms: measure { ActiveRecord::Base.connection.execute('SELECT 1') } }
rescue => e
{ status: 'error', message: e.message }
end
def check_redis
redis = Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'))
latency = measure { redis.ping }
{ status: 'ok', latency_ms: latency }
rescue => e
{ status: 'error', message: e.message }
end
def check_sidekiq
stats = Sidekiq::Stats.new
queue_size = stats.enqueued
dead_count = stats.dead_size
status = if dead_count > 100
'degraded' # Too many dead jobs
elsif queue_size > 1000
'degraded' # Queue backlog too large
else
'ok'
end
{ status: status, queued: queue_size, dead: dead_count, workers: stats.workers_size }
rescue => e
{ status: 'error', message: e.message }
end
def check_storage
# Check Active Storage or filesystem write access
ActiveStorage::Blob.service.exist?('health-check-probe')
{ status: 'ok' }
rescue => e
{ status: 'ok' } # Non-critical; don't fail health for this
end
def measure
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
end
end
Register the route:
# config/routes.rb
Rails.application.routes.draw do
get '/health', to: 'health#show'
get '/up', to: 'rails/health#show' # keep the Rails 7.1 default too
end
Now hit it:
curl https://api.yourdomain.com/health | jq
# {
# "status": "ok",
# "checks": {
# "database": { "status": "ok", "latency_ms": 1.2 },
# "redis": { "status": "ok", "latency_ms": 0.4 },
# "sidekiq": { "status": "ok", "queued": 12, "dead": 0, "workers": 4 },
# "storage": { "status": "ok" }
# },
# "rails_env": "production",
# "timestamp": "2026-06-30T10:00:00Z"
# }
Step 1: Set Up Vigilmon HTTP Monitoring
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- URL:
https://api.yourdomain.com/health - Check interval: 1 minute
- Expected response:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
3000ms
- Status code:
- Save the monitor
Vigilmon probes your endpoint from multiple geographic regions. If your Rails server, database, or Redis is down, the health endpoint returns 503 and Vigilmon fires an alert within a minute.
Also add a monitor for the default /up endpoint as a lightweight secondary check:
URL: https://api.yourdomain.com/up
Expected: status 200
Step 2: Heartbeat Monitoring for Sidekiq Recurring Jobs
Sidekiq jobs that run on a schedule (via sidekiq-cron, sidekiq-scheduler, or Clockwork) are invisible to HTTP monitoring. A job that stops running — because the Sidekiq process crashed, Redis became unreachable, or a scheduling configuration changed — will go undetected until someone notices stale data.
Use Vigilmon heartbeat monitors to detect missed jobs.
Set Up the Heartbeat in Vigilmon
- Go to Monitors → New Monitor → Heartbeat
- Name:
daily-report-job - Expected interval: 1 day
- Grace period: 30 minutes
- Save — copy the ping URL (e.g.,
https://vigilmon.online/heartbeat/abc123)
Wire the Ping Into Your Sidekiq Job
# app/jobs/daily_report_job.rb
class DailyReportJob
include Sidekiq::Job
sidekiq_options retry: 3, queue: :critical
def perform
Rails.logger.info "DailyReportJob: starting"
generate_and_send_reports
# Ping Vigilmon only on success
ping_vigilmon_heartbeat
Rails.logger.info "DailyReportJob: completed"
rescue => e
# Don't ping — Vigilmon will alert when the heartbeat is overdue
Rails.logger.error "DailyReportJob failed: #{e.message}"
raise
end
private
def generate_and_send_reports
# ... your job logic
end
def ping_vigilmon_heartbeat
heartbeat_url = ENV.fetch('VIGILMON_HEARTBEAT_DAILY_REPORT', nil)
return unless heartbeat_url
uri = URI(heartbeat_url)
Net::HTTP.get(uri)
rescue => e
# Don't let heartbeat ping failure affect the job
Rails.logger.warn "Vigilmon heartbeat ping failed: #{e.message}"
end
end
For sidekiq-cron scheduling:
# config/sidekiq_cron.yml
daily_report:
cron: "0 2 * * *"
class: DailyReportJob
queue: critical
Now if the job doesn't report in by 2:30 AM, Vigilmon alerts immediately.
Step 3: Monitoring Sidekiq Web UI
Sidekiq ships with a web UI (/sidekiq) that's a useful operational view. If you mount it in your Rails app, you can add a lightweight HTTP monitor for it — not as a health check, but as a confirmation that the routing layer is working:
# config/routes.rb
require 'sidekiq/web'
Rails.application.routes.draw do
authenticate :user, ->(u) { u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
end
# Monitor the Sidekiq Web ping endpoint (no auth required)
curl https://api.yourdomain.com/sidekiq/stats
Or use a separate internal health check specifically for Sidekiq status that your health controller already provides.
Step 4: Alert Channels — Slack and Webhook
Slack Alerting
- Create an Incoming Webhook in your Slack workspace (Settings → Integrations → Webhooks)
- Choose the
#incidentsor#rails-alertschannel - In Vigilmon, go to Alert Channels → New Channel → Webhook
- Paste the Slack Incoming Webhook URL
- Assign the channel to your Rails monitors
When your Rails API goes down, your Slack channel gets:
🔴 [DOWN] api.yourdomain.com/health
Started: 2026-06-30 10:23:00 UTC
Duration: 2 minutes
Regions failing: us-east, eu-west
Custom Webhook for PagerDuty Integration
If you're on-call rotation uses PagerDuty, point the webhook at PagerDuty's Events API v2:
# lib/pagerduty_webhook_handler.rb
# (your own small webhook receiver that translates Vigilmon payloads to PagerDuty)
class PagerdutyWebhookHandler
PAGERDUTY_API_URL = 'https://events.pagerduty.com/v2/enqueue'
def self.handle(vigilmon_payload)
event_action = vigilmon_payload['status'] == 'down' ? 'trigger' : 'resolve'
payload = {
routing_key: ENV.fetch('PAGERDUTY_ROUTING_KEY'),
event_action: event_action,
dedup_key: "vigilmon-#{vigilmon_payload['monitor_id']}",
payload: {
summary: "#{vigilmon_payload['monitor_name']} is #{vigilmon_payload['status']}",
source: vigilmon_payload['url'],
severity: 'critical',
timestamp: vigilmon_payload['started_at']
}
}
HTTParty.post(PAGERDUTY_API_URL, body: payload.to_json,
headers: { 'Content-Type' => 'application/json' })
end
end
Production Checklist
| Item | How to monitor |
|---|---|
| Rails app running | Vigilmon HTTP → /up |
| Database connectivity | Vigilmon HTTP → /health (checks DB in response) |
| Redis connectivity | Vigilmon HTTP → /health (checks Redis in response) |
| Sidekiq queue depth | Vigilmon HTTP → /health (includes queue stats) |
| Scheduled jobs running | Vigilmon heartbeat per job |
| SSL certificate expiry | Vigilmon automatic TLS monitoring |
| Response time | Vigilmon response time threshold alert |
| Alerts delivery | Slack webhook + PagerDuty |
Environment Configuration
Keep your Vigilmon credentials in Rails credentials or environment variables — never hardcode them:
# config/credentials.yml.enc (via rails credentials:edit)
vigilmon:
heartbeat_daily_report: https://vigilmon.online/heartbeat/abc123
heartbeat_weekly_digest: https://vigilmon.online/heartbeat/def456
# config/application.rb or an initializer
VIGILMON_HEARTBEAT_DAILY = Rails.application.credentials.dig(:vigilmon, :heartbeat_daily_report)
Getting Started
The shortest path to a monitored Rails production API:
- Confirm
/upis accessible:curl https://api.yourdomain.com/up - Add the custom
HealthControllerwith database + Redis checks - Create a Vigilmon HTTP monitor at vigilmon.online targeting
/health - Add a heartbeat monitor for each Sidekiq scheduled job
- Wire up the Slack webhook for immediate alert delivery
Free tier at vigilmon.online — no credit card required, up and running in under five minutes. Your Rails API is in production; now make sure you're the first to know when something goes wrong.