Framework Guide
Go API Monitoring
Set up health check endpoints and uptime monitoring for your Go API. Covers net/http, Gin, Echo, database health checks, and graceful shutdown.
Why Monitor Your Go API?
Go services are fast and reliable, but they still fail in production. Dependency outages, resource exhaustion, and deployment issues can take your API offline without any log output. External monitoring catches these failures before your users do.
- Goroutine leaks -- Leaked goroutines silently consume memory and file descriptors until the process crashes
- Connection pool exhaustion -- Database and HTTP client pools fill up under load, causing requests to hang
- Dependency failures -- Database, cache, or third-party API outages cascade through your service
- OOM kills -- Go's garbage collector reduces but does not eliminate memory pressure; containers get killed silently
Health Check Response Struct
Define a struct with JSON tags for consistent, type-safe health responses. This pattern works with any router or framework.
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version,omitempty"`
Checks map[string]string `json:"checks,omitempty"`
}
type DetailedHealth struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Goroutines int `json:"goroutines"`
Memory MemoryStats `json:"memory"`
Checks map[string]Check `json:"checks"`
}
type MemoryStats struct {
Alloc uint64 `json:"alloc_bytes"`
TotalAlloc uint64 `json:"total_alloc_bytes"`
Sys uint64 `json:"sys_bytes"`
NumGC uint32 `json:"num_gc"`
}
type Check struct {
Status string `json:"status"`
ResponseTime string `json:"response_time,omitempty"`
Error string `json:"error,omitempty"`
}
Basic Health Endpoint (net/http)
The standard library is all you need. Go 1.22+ supports method-based routing with http.ServeMux.
package main
import (
"encoding/json"
"log"
"net/http"
"runtime"
"time"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
resp := DetailedHealth{
Status: "healthy",
Timestamp: time.Now().UTC().Format(time.RFC3339),
Version: "1.0.0",
Goroutines: runtime.NumGoroutine(),
Memory: MemoryStats{
Alloc: mem.Alloc,
TotalAlloc: mem.TotalAlloc,
Sys: mem.Sys,
NumGC: mem.NumGC,
},
Checks: make(map[string]Check),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Printf("listening on %s", srv.Addr)
log.Fatal(srv.ListenAndServe())
}
Gin Health Endpoint
package main
import (
"net/http"
"runtime"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Simple liveness check
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, HealthResponse{
Status: "healthy",
Timestamp: time.Now().UTC().Format(time.RFC3339),
})
})
// Readiness check with dependencies
r.GET("/ready", func(c *gin.Context) {
checks := make(map[string]string)
status := http.StatusOK
// Check database (see database section below)
if err := db.PingContext(c.Request.Context()); err != nil {
checks["database"] = "error: " + err.Error()
status = http.StatusServiceUnavailable
} else {
checks["database"] = "ok"
}
statusText := "ready"
if status != http.StatusOK {
statusText = "not ready"
}
c.JSON(status, HealthResponse{
Status: statusText,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: checks,
})
})
r.Run(":8080")
}
Echo Health Endpoint
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, HealthResponse{
Status: "healthy",
Timestamp: time.Now().UTC().Format(time.RFC3339),
})
})
e.GET("/ready", func(c echo.Context) error {
ctx := c.Request().Context()
checks := make(map[string]string)
status := http.StatusOK
if err := db.PingContext(ctx); err != nil {
checks["database"] = "error: " + err.Error()
status = http.StatusServiceUnavailable
} else {
checks["database"] = "ok"
}
statusText := "ready"
if status != http.StatusOK {
statusText = "not ready"
}
return c.JSON(status, HealthResponse{
Status: statusText,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: checks,
})
})
e.Start(":8080")
}
Database Health Checks
Always use context with a timeout when checking database connectivity. This prevents a slow database from hanging your health endpoint.
database/sql
import (
"context"
"database/sql"
"time"
)
func checkDatabase(db *sql.DB) Check {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
start := time.Now()
err := db.PingContext(ctx)
duration := time.Since(start)
if err != nil {
return Check{
Status: "error",
ResponseTime: duration.String(),
Error: err.Error(),
}
}
return Check{
Status: "ok",
ResponseTime: duration.String(),
}
}
pgx (PostgreSQL)
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func checkPostgres(pool *pgxpool.Pool) Check {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
start := time.Now()
err := pool.Ping(ctx)
duration := time.Since(start)
if err != nil {
return Check{
Status: "error",
ResponseTime: duration.String(),
Error: err.Error(),
}
}
return Check{
Status: "ok",
ResponseTime: duration.String(),
}
}
Complete Health Handler with Dependencies
func healthHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
checks := map[string]Check{
"database": checkDatabase(db),
}
status := "healthy"
httpStatus := http.StatusOK
for _, check := range checks {
if check.Status != "ok" {
status = "unhealthy"
httpStatus = http.StatusServiceUnavailable
break
}
}
resp := DetailedHealth{
Status: status,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Version: version,
Goroutines: runtime.NumGoroutine(),
Memory: MemoryStats{
Alloc: mem.Alloc,
TotalAlloc: mem.TotalAlloc,
Sys: mem.Sys,
NumGC: mem.NumGC,
},
Checks: checks,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(resp)
}
}
Graceful Shutdown
Graceful shutdown is essential for zero-downtime deployments. Without it, in-flight requests get dropped during deploys, and monitoring services like UptimeSignal may flag false downtime alerts.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler(db))
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in a goroutine
go func() {
log.Printf("listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down gracefully...")
// Give in-flight requests 30 seconds to finish
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("forced shutdown: %v", err)
}
log.Println("server stopped")
}
Set Up Monitoring with UptimeSignal
Health endpoints are only useful if something checks them continuously. Internal health checks miss network-level failures, DNS issues, and infrastructure problems. External monitoring from UptimeSignal verifies your API is reachable from the outside.
- Deploy your health endpoint -- Make
/healthavailable on your production domain - Add the monitor -- In UptimeSignal, create a new HTTP monitor pointing to
https://your-api.com/health - Set the interval -- 5 minutes on the free tier, 1 minute on Pro
- Configure alerts -- Get notified by email when your API goes down or recovers
UptimeSignal checks your endpoint from outside your infrastructure, catching failures that internal health checks miss: DNS resolution errors, TLS certificate expiry, load balancer misconfigurations, and network-level outages.
Best Practices
- Always use context with timeouts -- Never let a slow dependency hang your health endpoint. Use
context.WithTimeoutfor every external call. - Keep it fast -- Health checks should respond in under 500ms. Go makes this easy with goroutines for parallel dependency checks.
- No auth required -- Monitoring services need unauthenticated access to your health endpoint.
- Separate liveness from readiness -- Use
/healthfor "is the process alive?" and/readyfor "can it serve traffic?" - Set server timeouts -- Always configure
ReadTimeout,WriteTimeout, andIdleTimeouton yourhttp.Server. - Use struct responses -- Define typed structs with json tags instead of
map[string]interface{}for consistent, documented responses.