tutorial

Monitoring River (Go Job Queue) with Vigilmon: Uptime, Heartbeats & Alerts

Learn how to monitor your River Go job queue — HTTP dashboard, PostgreSQL connectivity, worker pool status, and heartbeat pings for scheduled jobs.

River is a robust, PostgreSQL-backed job queue for Go. Because River runs entirely inside your database, monitoring it means watching three things: the River API/dashboard HTTP endpoint, the PostgreSQL connection, and whether your scheduled jobs are actually firing on time. Vigilmon covers all three. This tutorial wires River into Vigilmon in about 20 minutes.

What You'll Build

  • A /health endpoint that checks River's PostgreSQL connectivity and worker pool
  • A Vigilmon HTTP monitor pointed at that endpoint
  • A Vigilmon heartbeat ping sent by each scheduled River job
  • Email and Slack webhook alerts

Prerequisites

  • Go 1.21+
  • A River project (github.com/riverqueue/river v0.0.x or later)
  • PostgreSQL 14+ (River's backing store)
  • A free Vigilmon account

Step 1: Expose a Health Endpoint

River workers embed themselves in your Go process. Expose an HTTP health route that validates the PostgreSQL connection River depends on and reports whether the client is actively processing jobs.

// internal/health/river.go
package health

import (
    "context"
    "encoding/json"
    "net/http"
    "time"

    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/riverqueue/river"
)

type RiverHealthHandler struct {
    Pool   *pgxpool.Pool
    Client *river.Client[pgx.Tx]
}

type HealthResponse struct {
    Status    string            `json:"status"`
    Timestamp time.Time         `json:"timestamp"`
    Checks    map[string]string `json:"checks"`
}

func (h *RiverHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
    defer cancel()

    checks := map[string]string{}
    status := "ok"

    // Check PostgreSQL connectivity
    if err := h.Pool.Ping(ctx); err != nil {
        checks["postgres"] = "error: " + err.Error()
        status = "degraded"
    } else {
        checks["postgres"] = "ok"
    }

    // Confirm the River client is running (not stopped/drained)
    if h.Client.Stopped() {
        checks["river_client"] = "stopped"
        status = "degraded"
    } else {
        checks["river_client"] = "ok"
    }

    code := http.StatusOK
    if status != "ok" {
        code = http.StatusServiceUnavailable
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(HealthResponse{
        Status:    status,
        Timestamp: time.Now().UTC(),
        Checks:    checks,
    })
}

Register the handler in main.go:

mux := http.NewServeMux()
mux.Handle("/health", &health.RiverHealthHandler{Pool: pool, Client: riverClient})

go http.ListenAndServe(":8080", mux)

Test it:

curl -s localhost:8080/health | jq
# {
#   "status": "ok",
#   "timestamp": "2025-06-29T10:00:00Z",
#   "checks": { "postgres": "ok", "river_client": "ok" }
# }

Any non-2xx response causes Vigilmon to fire an alert — so a stopped client or a dead PostgreSQL connection surfaces immediately.


Step 2: Create a Vigilmon HTTP Monitor

Log in to Vigilmon and create a new HTTP Monitor:

| Field | Value | |---|---| | URL | https://jobs.yourdomain.com/health | | Method | GET | | Check interval | 60 seconds | | Expected status | 200 | | Timeout | 10 seconds | | Regions | Select 2–3 for triangulation |

Under Alert Channels, add your email address. You'll add Slack in Step 5.

Click Save — Vigilmon immediately starts polling. The first green tick appears within a minute.

Pro tip: If your health endpoint is on an internal network, use Vigilmon's private location agent or expose the endpoint only on a loopback-bound port behind your API gateway with a monitoring-only auth token.


Step 3: Heartbeat Monitoring for Scheduled River Jobs

HTTP uptime checks tell you the process is alive, but they don't tell you whether your periodic River jobs are actually executing. The River heartbeat pattern sends a Vigilmon ping every time a job runs successfully. If jobs stop firing — due to a cron misconfiguration, a panicking worker, or a full job queue — the heartbeat window expires and you're alerted.

First, grab your Heartbeat URL from Vigilmon (Dashboard → Heartbeat Monitors → New):

https://vigilmon.online/api/heartbeats/YOUR-UUID/ping

Set the period to slightly longer than your job's schedule interval — e.g. 90 seconds for a 60-second cron job.

Now create a River worker that pings Vigilmon on successful completion:

// internal/jobs/scheduled_report.go
package jobs

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/riverqueue/river"
)

// ScheduledReportArgs are the job arguments River serialises to PostgreSQL.
type ScheduledReportArgs struct{}

func (ScheduledReportArgs) Kind() string { return "scheduled_report" }

type ScheduledReportWorker struct {
    river.WorkerDefaults[ScheduledReportArgs]
    httpClient *http.Client
}

func NewScheduledReportWorker() *ScheduledReportWorker {
    return &ScheduledReportWorker{
        httpClient: &http.Client{Timeout: 5 * time.Second},
    }
}

func (w *ScheduledReportWorker) Work(ctx context.Context, job *river.Job[ScheduledReportArgs]) error {
    // --- do your actual job work here ---
    if err := generateReport(ctx); err != nil {
        return fmt.Errorf("report generation failed: %w", err)
    }

    // Ping Vigilmon only on success — silence = failure
    heartbeatURL := os.Getenv("VIGILMON_HEARTBEAT_URL")
    resp, err := w.httpClient.Get(heartbeatURL)
    if err != nil {
        log.Printf("[heartbeat] ping failed: %v", err)
        return nil // don't fail the job over a missed ping
    }
    resp.Body.Close()
    log.Printf("[heartbeat] pinged, status %d", resp.StatusCode)
    return nil
}

func generateReport(ctx context.Context) error {
    // placeholder for real work
    return nil
}

Register the worker and add the periodic job entry:

workers := river.NewWorkers()
river.AddWorker(workers, jobs.NewScheduledReportWorker())

riverClient, _ := river.NewClient(riverpgxv5.New(pool), &river.Config{
    Queues: map[string]river.QueueConfig{
        river.QueueDefault: {MaxWorkers: 10},
    },
    Workers: workers,
    PeriodicJobs: []*river.PeriodicJob{
        river.NewPeriodicJob(
            river.EveryPeriod(60*time.Second),
            func() (river.JobArgs, *river.InsertOpts) {
                return jobs.ScheduledReportArgs{}, nil
            },
            &river.PeriodicJobOpts{RunOnStart: true},
        ),
    },
})

Store the heartbeat URL in the environment:

VIGILMON_HEARTBEAT_URL=https://vigilmon.online/api/heartbeats/YOUR-UUID/ping

Step 4: Monitor the River Dashboard (Optional)

River ships with an optional HTTP dashboard at /river when you embed the riverui package. If you've enabled it, add a second Vigilmon monitor for that URL:

| Field | Value | |---|---| | URL | https://jobs.yourdomain.com/river | | Method | GET | | Expected status | 200 | | Check interval | 120 seconds |

This confirms the UI is reachable, which is useful for ops dashboards that your team checks during incidents.


Step 5: Alert Routing

Email Alerts

Already configured in Step 2. Vigilmon sends alerts when:

  • The /health endpoint returns a non-2xx status
  • The endpoint is unreachable within the timeout
  • The heartbeat window expires (scheduled job stopped firing)

Slack Webhook Alerts

  1. Create a Slack incoming webhook in your workspace (Slack docs)
  2. In Vigilmon → Alert Channels → Add Channel → Webhook
  3. Paste the Slack webhook URL
  4. Assign the channel to your River monitors

The alert payload Vigilmon posts:

{
  "text": "🔴 *jobs.yourdomain.com/health* is DOWN\nStatus: 503 | Region: us-east-1\nDuration: 3m 02s"
}

Step 6: Test the Full Loop

  1. Simulate a PostgreSQL failure: stop Postgres and hit /health — expect 503.
  2. Verify Vigilmon detects it: within one check interval, the dashboard goes red and an alert fires.
  3. Test heartbeat silence: disable the periodic job entry, wait for the heartbeat window to expire — you should receive an alert.
  4. Recover: bring Postgres back up and re-enable the job. Vigilmon auto-recovers and sends a "back online" notification.

Production Checklist

  • [ ] /health checks PostgreSQL connectivity and River client state
  • [ ] Heartbeat URL stored in environment variable, not in code
  • [ ] Heartbeat period set to 1.5× the job schedule interval
  • [ ] Heartbeat ping only sent on successful job completion
  • [ ] Alert channels tested end-to-end (email + Slack)
  • [ ] Maintenance windows configured for deploy pipeline

Wrapping Up

You now have production-grade monitoring for your River job queue:

  • Uptime monitoring that catches PostgreSQL or process failures within 60 seconds
  • Heartbeat monitoring that catches silent job-scheduling failures
  • Alert routing to email and Slack with escalation policies

Sign up for Vigilmon and get your first monitor running in under five minutes — no credit card required.

If you hit issues or have questions, drop a comment below. Happy monitoring!

Monitor your app with Vigilmon

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

Start free →