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.

  1. Deploy your health endpoint -- Make /health available on your production domain
  2. Add the monitor -- In UptimeSignal, create a new HTTP monitor pointing to https://your-api.com/health
  3. Set the interval -- 5 minutes on the free tier, 1 minute on Pro
  4. 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.WithTimeout for 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 /health for "is the process alive?" and /ready for "can it serve traffic?"
  • Set server timeouts -- Always configure ReadTimeout, WriteTimeout, and IdleTimeout on your http.Server.
  • Use struct responses -- Define typed structs with json tags instead of map[string]interface{} for consistent, documented responses.

Common Go Production Issues

Goroutine Leaks

Goroutines that block on channels, network calls, or mutexes without timeouts leak memory and file descriptors. Monitor runtime.NumGoroutine() in your health check. A steadily increasing count signals a leak.

Connection Pool Exhaustion

The default database/sql pool has no max open connections. Under load, it opens connections until the database rejects them. Set db.SetMaxOpenConns() and db.SetMaxIdleConns() explicitly, and include a database ping in your readiness check.

Missing Server Timeouts

The default http.Server has no read or write timeout. A slow client can hold connections open indefinitely, exhausting your file descriptor limit. Always set ReadTimeout, WriteTimeout, and IdleTimeout.

No Graceful Shutdown

Without graceful shutdown, deploys drop in-flight requests and cause brief downtime. Kubernetes readiness probes and external monitors like UptimeSignal will flag these gaps. Use srv.Shutdown(ctx) to drain connections cleanly.

Frequently Asked Questions

What should a Go health check endpoint return?
A health check should return HTTP 200 with a JSON body containing at minimum a status field. Use a struct with json tags for the response. For comprehensive checks, include database connectivity, goroutine count (runtime.NumGoroutine()), memory stats (runtime.MemStats), and app version. Return 503 if any critical dependency is unhealthy.
Should I use net/http or a framework like Gin?
Use whatever your application already uses. The standard library's http.ServeMux (improved in Go 1.22 with method routing) handles health routes efficiently with zero dependencies. Gin and Echo add convenience for larger APIs but are not required for health checks.
How do I implement graceful shutdown in Go?
Use os/signal.Notify to capture SIGINT and SIGTERM, then call srv.Shutdown(ctx) with a timeout context. This lets in-flight requests complete before the server stops. Graceful shutdown prevents false downtime alerts from monitoring services during deployments.
Should my Go health endpoint require authentication?
No. Health check endpoints should be unauthenticated so external monitoring services like UptimeSignal can reach them without managing credentials. If you need detailed diagnostics, return minimal data on /health and serve a detailed version behind auth middleware.
How often should I check my Go API health?
For production APIs, check every 1-5 minutes. UptimeSignal's free tier checks every 5 minutes, which catches most outages quickly. Pro tier supports 1-minute intervals for faster detection. Go services are typically fast responders, so health checks add negligible overhead even at 1-minute intervals.

Start monitoring your Go API

Get alerted when your Go endpoints fail. Setup takes 30 seconds.

25 monitors free. No credit card required.

More Framework Guides