How to monitor your Go web app with uptime checks and heartbeat monitoring (free)
Your Go service crashed overnight and you found out at 9 AM when the team Slack lit up. The binary was sitting there with a non-zero exit code since 2 AM. Nobody knew.
Go is fast, statically typed, and great for infrastructure tooling — but the standard library gives you exactly zero built-in observability. By the end of this article you'll have HTTP uptime monitoring, heartbeat checks for your cron jobs, Slack/Discord alerts, and a public status page — all in under 30 minutes, free.
The silent failure problem with Go services
Go services have two failure modes that go undetected without external monitoring:
Endpoint outages — your /health or /api/ handler starts returning 500s or panics. Users hit errors. You don't know until someone tells you.
Silent goroutine/cron failures — a goroutine that runs a scheduled job panics and gets recovered, or a ticker fires but the underlying operation silently fails. No crash. No log line that anyone is watching. Your background work just stops happening.
Both are easy to detect with the right tool actively checking from outside.
Step 1: Add a /health endpoint with net/http
Go's standard net/http package is all you need. Add a health handler that checks your critical dependencies:
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"time"
)
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Checks map[string]string `json:"checks"`
}
func healthHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
checks := map[string]string{}
status := "ok"
// Check database connectivity
if err := db.Ping(); err != nil {
checks["database"] = "error: " + err.Error()
status = "degraded"
} else {
checks["database"] = "ok"
}
resp := HealthResponse{
Status: status,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: checks,
}
w.Header().Set("Content-Type", "application/json")
if status != "ok" {
w.WriteHeader(http.StatusServiceUnavailable)
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(resp)
}
}
func main() {
// db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
mux := http.NewServeMux()
// mux.HandleFunc("/health", healthHandler(db))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok","timestamp":"` + time.Now().UTC().Format(time.RFC3339) + `"}`))
})
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
The key point: return 503 Service Unavailable when a dependency is unhealthy. This gives your monitoring tool a meaningful signal to alert on — not just "is the server up" but "is the server actually working."
Step 2: Set up HTTP uptime monitoring
With your /health endpoint live, point Vigilmon at it:
- Sign up for a free account at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval to 5 minutes (free tier)
- Save
Vigilmon probes your endpoint from multiple regions every 5 minutes. The moment it gets anything other than 2xx — or hits a timeout — it opens an incident and alerts you.
You can add multiple monitors for the same service:
https://yourdomain.com/health— deep health check including DBhttps://yourdomain.com/— confirms the frontend is servinghttps://yourdomain.com/api/v1/ping— confirms your API is reachable
Step 3: Heartbeat monitoring for Go cron jobs and background workers
HTTP monitoring catches server outages. But what about your ticker-based jobs, goroutines running on a schedule, or tasks kicked off by time.AfterFunc?
The heartbeat pattern: your job pings a URL at the end of every successful run. If Vigilmon stops receiving that ping within the expected window, it fires an alert.
Here's the pattern for a Go scheduled task:
package main
import (
"log"
"net/http"
"os"
"time"
)
func pingHeartbeat(url string) {
if url == "" {
return
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
log.Printf("heartbeat ping failed: %v", err)
return
}
resp.Body.Close()
}
func runDailyReport() error {
// your actual job logic here
log.Println("running daily report...")
time.Sleep(100 * time.Millisecond) // simulate work
return nil
}
func startScheduler() {
heartbeatURL := os.Getenv("REPORT_HEARTBEAT_URL")
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
if err := runDailyReport(); err != nil {
log.Printf("daily report failed: %v", err)
continue // don't ping on failure
}
pingHeartbeat(heartbeatURL)
}
}
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. every 24 hours)
- Copy the unique ping URL it generates
- Set it as your
REPORT_HEARTBEAT_URLenvironment variable
Now if your job panics, returns an error, or your ticker stops firing — Vigilmon alerts you within one missed interval.
One heartbeat per critical job
The pattern composes naturally. Give each important background job its own heartbeat URL:
var (
reportHeartbeat = os.Getenv("REPORT_HEARTBEAT_URL")
cleanupHeartbeat = os.Getenv("CLEANUP_HEARTBEAT_URL")
syncHeartbeat = os.Getenv("SYNC_HEARTBEAT_URL")
)
Treat background jobs like uptime monitors — each one gets its own check.
Step 4: Webhook alerts to Slack or Discord
Set up alert delivery in Vigilmon so you're notified the moment something breaks:
For Slack:
- Create an incoming webhook in your Slack workspace (Slack Apps → Incoming Webhooks)
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste the webhook URL
- Enable it on your monitors
For Discord:
- In your Discord server, go to channel settings → Integrations → Webhooks
- Create a new webhook and copy the URL
- In Vigilmon, go to Notifications → New Channel → Discord
- Paste the Discord webhook URL
You'll receive alerts like:
🔴 ALERT: yourdomain.com/health is DOWN
Checked from: US-East, EU-West
Status: 503 Service Unavailable
Started: 3 minutes ago
And when it recovers:
✅ RESOLVED: yourdomain.com/health is back UP
Downtime: 11 minutes
Step 5: Public status page for your Go service
A public status page lets users self-serve the "is it down for everyone?" question during an incident. This reduces support noise and builds trust.
In Vigilmon:
- Go to Status Pages → New Status Page
- Name it and choose which monitors to display
- Share the public URL (e.g.
status.yourdomain.com)
Link to it from your README, your app's docs, or your main site footer.
What you've built
In under 30 minutes:
| What | How |
|------|-----|
| HTTP uptime monitoring | /health handler + Vigilmon HTTP monitor |
| Dependency health checks | db.Ping() inside the health handler |
| Cron/job monitoring | Heartbeat ping at end of each scheduled task |
| Instant alerts | Slack/Discord webhook notifications |
| Public status page | Vigilmon status page |
The full setup costs $0 on the free tier and takes less time than debugging a silent cron failure that's been going on for a week.
Next steps
- Add response time monitoring to catch latency regressions before they become outages
- Create a heartbeat for every critical background goroutine — not just the obvious ones
- Use
context.WithTimeoutaround your health checks so a slow DB doesn't hang the handler
Get started free at vigilmon.online — no credit card, monitors running in under a minute.