Hotwire is the HTML-over-the-wire approach pioneered by Basecamp that combines Turbo (fast page navigation and real-time server push) with Stimulus (lightweight JavaScript controllers) to build fast, interactive web apps with minimal custom JavaScript. Hotwire is the default frontend stack for Rails 7+ and is increasingly used with Django, Laravel, and other server-side frameworks. But no matter how elegantly your Stimulus controllers are structured, your users notice the moment your Hotwire app goes down. Vigilmon gives you the full observability layer your Hotwire stack needs: HTTP uptime monitors, WebSocket/Redis health checks, heartbeat monitors for background workers, and instant alerts to email or Slack.
What You'll Build
- A
/healthendpoint covering your entire Hotwire stack (HTTP, database, Redis, Action Cable) - Vigilmon HTTP monitors for the app root and the health endpoint
- A heartbeat monitor for Sidekiq/background workers that drive Turbo Streams
- Stimulus-compatible uptime badge component
- Alert channels (email and Slack)
Prerequisites
- A Hotwire-powered app (Rails 7+, or another backend with
@hotwired/turboand@hotwired/stimulus) - A free Vigilmon account
Step 1: Add a Comprehensive Health Endpoint
Hotwire's real-time features rely on Redis and Action Cable in addition to the database. Your health check should validate the entire stack, not just HTTP.
Rails 7+ (canonical Hotwire stack)
# config/routes.rb
get "/health", to: "health#show"
# app/controllers/health_controller.rb
class HealthController < ApplicationController
# Allow unauthenticated access for monitoring
skip_before_action :authenticate_user!, raise: false
def show
checks = {}
degraded = false
# PostgreSQL / MySQL
begin
ActiveRecord::Base.connection.execute("SELECT 1")
checks[:database] = "ok"
rescue => e
checks[:database] = "error: #{e.message}"
degraded = true
end
# Redis (Turbo Streams + Action Cable)
begin
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
redis.ping
checks[:redis] = "ok"
rescue => e
checks[:redis] = "error: #{e.message}"
degraded = true
end
# Sidekiq queue depth (optional warning threshold)
begin
queue_size = Sidekiq::Queue.new.size
checks[:sidekiq_queue] = queue_size < 1000 ? "ok" : "warning: #{queue_size} jobs"
rescue => e
checks[:sidekiq_queue] = "error: #{e.message}"
end
# Active Storage (if used for file uploads)
begin
ActiveStorage::Blob.count
checks[:storage] = "ok"
rescue => e
checks[:storage] = "error: #{e.message}"
degraded = true
end if defined?(ActiveStorage)
render json: {
status: degraded ? "degraded" : "ok",
timestamp: Time.current.iso8601,
checks: checks,
framework: "hotwire",
turbo: Turbo::VERSION,
}, status: degraded ? :service_unavailable : :ok
end
end
Django + django-turbo-helper (Python)
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("health/", views.health_check, name="health"),
]
# views.py
from django.http import JsonResponse
from django.db import connection
import redis as redis_lib
import os
def health_check(request):
checks = {}
degraded = False
# Database
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"error: {e}"
degraded = True
# Redis
try:
r = redis_lib.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379/0"))
r.ping()
checks["redis"] = "ok"
except Exception as e:
checks["redis"] = f"error: {e}"
degraded = True
return JsonResponse(
{
"status": "degraded" if degraded else "ok",
"framework": "hotwire",
"checks": checks,
},
status=503 if degraded else 200,
)
Test it:
curl http://localhost:3000/health | jq .
# {
# "status": "ok",
# "timestamp": "2026-07-03T...",
# "checks": {"database": "ok", "redis": "ok", "sidekiq_queue": "ok"},
# "framework": "hotwire",
# "turbo": "7.x.x"
# }
Step 2: Configure Vigilmon HTTP Monitors
Log in to Vigilmon → Monitors → New Monitor.
Monitor 1: App Root
| Field | Value |
|---|---|
| URL | https://yourapp.com/ |
| Method | GET |
| Expected status | 200 |
| Expected body | A stable string (e.g. data-controller or your app title) |
| Check interval | 1 minute |
Monitor 2: Health Endpoint
| Field | Value |
|---|---|
| URL | https://yourapp.com/health |
| Method | GET |
| Expected status | 200 |
| Expected body | "status":"ok" |
| Check interval | 1 minute |
Two monitors, two failure domains. The root check catches Puma/Nginx failures; the health check catches database and Redis failures that would break Turbo Streams silently.
Step 3: Heartbeat for Background Workers
Turbo Streams and real-time Hotwire features typically rely on Sidekiq (or another job processor) broadcasting updates. A Vigilmon heartbeat catches silent worker failures before your users notice frozen live feeds.
Sidekiq recurring job
# app/jobs/hotwire_sync_job.rb
class HotwireSyncJob < ApplicationJob
queue_as :default
def perform
# Your actual sync/broadcast logic
LiveFeedService.broadcast_updates!
# Ping heartbeat only on success
heartbeat_url = ENV["VIGILMON_HEARTBEAT_URL"]
if heartbeat_url.present?
uri = URI(heartbeat_url)
Net::HTTP.get(uri)
end
rescue => e
Rails.logger.error "[HotwireSyncJob] Failed: #{e.message}"
# No heartbeat ping → Vigilmon alerts after the grace window expires
raise
end
end
Configure a recurring schedule (using sidekiq-cron or good_job):
# config/schedule.yml (sidekiq-cron)
hotwire_sync:
cron: "*/5 * * * *"
class: "HotwireSyncJob"
queue: default
Set up the heartbeat in Vigilmon:
- Monitors → New Heartbeat Monitor
- Expected interval: 10 minutes (2× the 5-minute cron)
- Copy the ping URL and add to environment:
VIGILMON_HEARTBEAT_URL=https://vigilmon.online/api/heartbeat/xxxxxxxx
Step 4: Add an Uptime Badge with a Stimulus Controller
Use a lightweight Stimulus controller to lazy-load the Vigilmon badge — in the Hotwire spirit of progressive enhancement.
// app/javascript/controllers/uptime_badge_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
badgeUrl: String,
statusUrl: String
}
connect() {
const img = document.createElement("img")
img.src = this.badgeUrlValue
img.alt = "Uptime"
img.width = 120
img.height = 20
img.style.opacity = "0"
img.style.transition = "opacity 0.2s"
img.addEventListener("load", () => { img.style.opacity = "1" })
const link = document.createElement("a")
link.href = this.statusUrlValue
link.target = "_blank"
link.rel = "noopener noreferrer"
link.setAttribute("aria-label", "Service uptime status")
link.appendChild(img)
this.element.appendChild(link)
}
}
Register the controller (Stimulus 3 with importmap or bundler):
// app/javascript/controllers/index.js
import { application } from "./application"
import UptimeBadgeController from "./uptime_badge_controller"
application.register("uptime-badge", UptimeBadgeController)
Add the element to your layout:
<!-- app/views/layouts/application.html.erb -->
<footer>
<div data-controller="uptime-badge"
data-uptime-badge-badge-url-value="https://vigilmon.online/api/badge/your-monitor-id.svg"
data-uptime-badge-status-url-value="https://vigilmon.online/status/your-monitor-slug">
</div>
</footer>
Because Turbo Drive preserves the layout between navigations, the badge loads once and persists for the user's entire session.
Step 5: Set Up Alert Channels
In Vigilmon → Alert Channels:
- Alert Channels → Email → add your on-call address
- Attach to both HTTP monitors and the heartbeat
Slack
- Create a Slack Incoming Webhook for
#alerts - Alert Channels → Webhook → paste the URL
- Payload template:
{
"text": "🚨 *{{ monitor.name }}* is DOWN\n{{ monitor.url }}\nStatus: {{ event.status }}\n<https://vigilmon.online|Open Vigilmon>"
}
Step 6: Verify the Entire Hotwire Stack
# 1. Health endpoint returns 200 with all checks green
curl -s https://yourapp.com/health | jq .
# 2. Stop Redis → confirm health returns 503 with redis error
# Expected: Vigilmon alerts within 2 minutes (catches silent Turbo Stream failures)
# 3. Stop Sidekiq → wait for heartbeat window to expire
# Expected: heartbeat alert fires before users notice frozen live updates
# 4. Vigilmon → "Test Alert" → verify email and Slack delivery
# 5. Navigate between pages in your Hotwire app
# Expected: badge remains visible (Turbo Drive preserves layout)
Summary
| Monitor | What It Catches | |---|---| | HTTP root check | Puma/Nginx/server failures, broken deploys | | HTTP health endpoint | Database, Redis, and Action Cable failures | | Heartbeat | Silent Sidekiq failures, frozen Turbo Stream broadcasts |
Next Steps
- Add a dedicated Sidekiq Web UI monitor for job queue depth visibility
- Use Vigilmon's response time graphs to correlate Stimulus controller latency with Action Cable broadcast timing
- Configure separate monitors for your Action Cable WebSocket endpoint
- Set up multi-channel alerting: immediate Slack for Redis failures, email digest for queue depth warnings
Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.