go 33 lines · 1 tab

Readiness and liveness probes with dependency checks

Leah Thompson Jan 2026
1 tab
package health

import (
  "context"
  "net"
  "net/http"
  "time"
)

type Checker interface {
  Ping(ctx context.Context) error
}

func Liveness(w http.ResponseWriter, _ *http.Request) {
  w.WriteHeader(http.StatusOK)
  _, _ = w.Write([]byte("ok"))
}

func Readiness(chk Checker) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond)
    defer cancel()

    if err := chk.Ping(ctx); err != nil {
      w.WriteHeader(http.StatusServiceUnavailable)
      _, _ = w.Write([]byte("not_ready"))
      return
    }

    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte("ready"))
  }
}
1 file · go Explain with highlit

I separate liveness from readiness because they answer different questions. Liveness is “is the process alive enough to respond?” and should be cheap; readiness is “can this instance take traffic?” and can include dependency checks like DB connectivity or queue health. The common mistake is making readiness too expensive or flaky, which causes cascading restarts. I keep readiness fast by using short timeouts and a small set of essential checks (DB ping, migrations applied, critical config loaded). This snippet shows a simple approach: two endpoints, explicit status codes, and dependency checks behind a small interface so it’s testable. It’s boring, but it prevents a lot of noisy outages during deploys.


Related snips

Share this code

Here's the card — post it anywhere.

Readiness and liveness probes with dependency checks — share card
Link copied