tutorial

Ruby on Rails API Monitoring: Health Checks, Sidekiq, and Vigilmon Setup

A complete guide to monitoring Ruby on Rails APIs — the /up endpoint in Rails 7.1+, custom health controllers, Sidekiq and Redis monitoring, and setting up Vigilmon with Slack alerting.

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

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. URL: https://api.yourdomain.com/health
  4. Check interval: 1 minute
  5. Expected response:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 3000ms
  6. 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

  1. Go to Monitors → New Monitor → Heartbeat
  2. Name: daily-report-job
  3. Expected interval: 1 day
  4. Grace period: 30 minutes
  5. 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

  1. Create an Incoming Webhook in your Slack workspace (Settings → Integrations → Webhooks)
  2. Choose the #incidents or #rails-alerts channel
  3. In Vigilmon, go to Alert Channels → New Channel → Webhook
  4. Paste the Slack Incoming Webhook URL
  5. 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:

  1. Confirm /up is accessible: curl https://api.yourdomain.com/up
  2. Add the custom HealthController with database + Redis checks
  3. Create a Vigilmon HTTP monitor at vigilmon.online targeting /health
  4. Add a heartbeat monitor for each Sidekiq scheduled job
  5. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →