tutorial

How to Monitor Turbo with Vigilmon

Add uptime monitoring, heartbeat checks, and instant alerts to your Turbo (Hotwire) server-rendered app with Vigilmon.

Turbo is the engine at the heart of Hotwire — it accelerates server-rendered HTML apps with Turbo Drive (fast page changes without full reloads), Turbo Frames (independent page fragments), and Turbo Streams (real-time server push over WebSocket or SSE). Together they let you ship rich, reactive interfaces without a heavy JavaScript framework. But Turbo's speed advantage vanishes the moment your Rails, Django, or Laravel backend is unavailable. Vigilmon gives you continuous HTTP uptime monitoring, WebSocket-aware health checks, and instant alerts when your Turbo-powered app goes down.

What You'll Build

  • A /health endpoint covering database, Redis, and Action Cable
  • Vigilmon HTTP monitors for the app and the health endpoint
  • A heartbeat for background jobs (Sidekiq, Celery, Horizon)
  • Alert channels (email and Slack)
  • An uptime badge in your Turbo layout

Prerequisites

  • A Turbo-powered app (Rails with Hotwire, or any backend with @hotwired/turbo)
  • A free Vigilmon account

Step 1: Add a Health Endpoint

Turbo Streams rely on Action Cable (or another WebSocket adapter) in addition to HTTP. Your health check should cover both layers.

Ruby on Rails (canonical Turbo stack)

# config/routes.rb
get "/health", to: "health#show"

# app/controllers/health_controller.rb
class HealthController < ApplicationController
  skip_before_action :authenticate_user!, raise: false

  def show
    checks = {}
    degraded = false

    # Database
    begin
      ActiveRecord::Base.connection.execute("SELECT 1")
      checks[:database] = "ok"
    rescue => e
      checks[:database] = "error: #{e.message}"
      degraded = true
    end

    # Redis (used for Action Cable and Turbo Streams)
    begin
      Redis.current.ping
      checks[:redis] = "ok"
    rescue => e
      checks[:redis] = "error: #{e.message}"
      degraded = true
    end

    # Action Cable adapter
    begin
      ActionCable.server.config.cable.fetch("adapter")
      checks[:action_cable] = "configured"
    rescue => e
      checks[:action_cable] = "error: #{e.message}"
      degraded = true
    end

    render json: {
      status: degraded ? "degraded" : "ok",
      timestamp: Time.current.iso8601,
      checks: checks
    }, status: degraded ? :service_unavailable : :ok
  end
end

Node.js backend with @hotwired/turbo

// server.js (Express)
import express from "express";
import { createClient } from "redis";

const app = express();
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

app.get("/health", async (req, res) => {
  const checks = {};
  let degraded = false;

  try {
    // await db.query("SELECT 1");
    checks.database = "ok";
  } catch (err) {
    checks.database = `error: ${err.message}`;
    degraded = true;
  }

  try {
    await redis.ping();
    checks.redis = "ok";
  } catch (err) {
    checks.redis = `error: ${err.message}`;
    degraded = true;
  }

  res.status(degraded ? 503 : 200).json({
    status: degraded ? "degraded" : "ok",
    timestamp: new Date().toISOString(),
    checks,
  });
});

app.listen(process.env.PORT ?? 3000);

Test it:

curl http://localhost:3000/health | jq .
# {"status":"ok","timestamp":"2026-07-03T...","checks":{"database":"ok","redis":"ok","action_cable":"configured"}}

Step 2: Configure Vigilmon HTTP Monitors

Log in to VigilmonMonitors → New Monitor.

Monitor 1: App Root

| Field | Value | |---|---| | URL | https://yourapp.com/ | | Method | GET | | Expected status | 200 | | Expected body | A stable string from your HTML (e.g. app title or data-turbo) | | 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 |

With two monitors, Vigilmon catches web server failures independently from database and Redis failures — which is critical for Turbo Streams since Redis failure kills real-time updates even when HTTP pages still load.


Step 3: Heartbeat for Background Workers

Turbo Streams often broadcast from background jobs. A Vigilmon heartbeat catches silent Sidekiq/Resque/GoodJob worker failures before users notice frozen updates.

Sidekiq example

# app/jobs/broadcast_job.rb
class BroadcastJob < ApplicationJob
  queue_as :default

  def perform(record_id)
    record = MyRecord.find(record_id)

    # Broadcast Turbo Stream update
    Turbo::StreamsChannel.broadcast_replace_to(
      "my_record_#{record.id}",
      target: "record_#{record.id}",
      partial: "records/record",
      locals: { record: record }
    )

    # Ping heartbeat on success
    if (url = ENV["VIGILMON_HEARTBEAT_URL"]).present?
      Net::HTTP.get(URI(url))
    end
  rescue => e
    Rails.logger.error "[BroadcastJob] Failed: #{e.message}"
    raise # No heartbeat ping → Vigilmon alerts
  end
end

Set up the heartbeat in Vigilmon:

  1. Monitors → New Heartbeat Monitor
  2. Expected interval: 2× your job frequency
  3. Copy the ping URL and add to your environment:
VIGILMON_HEARTBEAT_URL=https://vigilmon.online/api/heartbeat/xxxxxxxx

Step 4: Add an Uptime Badge to Your Turbo Layout

Add the badge to your application layout. Turbo Drive preserves your layout across navigations, so the badge appears on every page:

<!-- Rails: app/views/layouts/application.html.erb -->
<footer>
  <a href="https://vigilmon.online/status/your-monitor-slug"
     target="_blank"
     rel="noopener noreferrer"
     aria-label="Service uptime status">
    <img src="https://vigilmon.online/api/badge/your-monitor-id.svg"
         alt="Uptime"
         width="120"
         height="20">
  </a>
</footer>

Because Turbo Drive only replaces <body> content (keeping <head> and persistent elements), you can also place the badge inside a data-turbo-permanent element so it survives every navigation:

<div data-turbo-permanent id="status-badge">
  <a href="https://vigilmon.online/status/your-monitor-slug"
     target="_blank" rel="noopener noreferrer">
    <img src="https://vigilmon.online/api/badge/your-monitor-id.svg"
         alt="Uptime" width="120" height="20">
  </a>
</div>

Step 5: Set Up Alert Channels

In Vigilmon → Alert Channels:

Email

  1. Alert Channels → Email → add your on-call address
  2. Attach to both HTTP monitors and the heartbeat

Slack

  1. Create a Slack Incoming Webhook for #alerts
  2. Alert Channels → Webhook → paste the URL
  3. Payload template:
{
  "text": "🚨 *{{ monitor.name }}* is DOWN\n{{ monitor.url }}\nStatus: {{ event.status }}\n<https://vigilmon.online|Open Vigilmon>"
}

Step 6: Verify the Setup

# 1. Health endpoint returns 200
curl -s https://yourapp.com/health | jq .

# 2. Stop Redis → confirm health returns 503
# Expected: Vigilmon alerts within 2 minutes

# 3. Stop Sidekiq → wait for heartbeat window to expire
# Expected: heartbeat alert fires

# 4. Vigilmon → "Test Alert" → verify email/Slack delivery

Summary

| Monitor | What It Catches | |---|---| | HTTP root check | Web server and deployment failures | | HTTP health endpoint | Database, Redis, and Action Cable failures | | Heartbeat | Silent Sidekiq/worker failures, broken Turbo Streams |


Next Steps

  • Add a dedicated Redis monitor for Turbo Streams uptime independent of the app health check
  • Use Vigilmon's response time graphs to correlate Turbo Frame slow renders with database or job queue latency
  • Set up multi-channel alerting: Slack for immediate, email for digest

Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.

Monitor your app with Vigilmon

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

Start free →