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
/healthendpoint 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/riverv0.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
/healthendpoint returns a non-2xx status - The endpoint is unreachable within the timeout
- The heartbeat window expires (scheduled job stopped firing)
Slack Webhook Alerts
- Create a Slack incoming webhook in your workspace (Slack docs)
- In Vigilmon → Alert Channels → Add Channel → Webhook
- Paste the Slack webhook URL
- 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
- Simulate a PostgreSQL failure: stop Postgres and hit
/health— expect503. - Verify Vigilmon detects it: within one check interval, the dashboard goes red and an alert fires.
- Test heartbeat silence: disable the periodic job entry, wait for the heartbeat window to expire — you should receive an alert.
- Recover: bring Postgres back up and re-enable the job. Vigilmon auto-recovers and sends a "back online" notification.
Production Checklist
- [ ]
/healthchecks 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!