How to Add a Go Health Check Endpoint
A Go health check endpoint does two things: confirm the process is running and confirm it can reach the database. Return 200 with {"status":"healthy"} on success, 503 with {"status":"unhealthy"} on failure - that is all a load balancer needs to route traffic safely.
Why a health check needs to ping the DB
A handler that always returns 200 OK is worse than no health check at all - it
tells the load balancer the service is ready even when the database is down and
every real request will fail. The check must exercise whatever the service depends
on. For most Go APIs that means db.Ping().
Adding the handler
Register /health alongside your existing routes and call db.Ping():
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := db.Ping(); err != nil {
w.WriteHeader(http.StatusServiceUnavailable) // 503
json.NewEncoder(w).Encode(map[string]string{"status": "unhealthy"})
return
}
w.WriteHeader(http.StatusOK) // 200
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
}
Wire it up where you register your other routes:
http.HandleFunc("/items", handleItems)
http.HandleFunc("/health", handleHealth)
Rebuild and restart after editing main.go:
go build -o my-api .
pkill -f my-api; ./my-api &
curl -s http://localhost:4000/health
# {"status":"healthy"}
What db.Ping() actually does
db.Ping() borrows a connection from the pool, sends a lightweight round-trip
to the database, and returns it. It is not free - it opens a real connection if
the pool is empty - but it is cheap enough to run on every health probe (which
typically fires every few seconds). If the DB is unreachable, Ping() returns
an error and you surface that as 503.
Splitting liveness from readiness
In production Kubernetes deployments, one health endpoint is not always enough:
/livez- is the process alive? Return200immediately, no DB check. Kubernetes uses this as a liveness probe: failure here triggers a pod restart./readyz- can the pod serve traffic? Rundb.Ping()here. Kubernetes uses this as a readiness probe: failure here removes the pod from the Service endpoints without restarting it.
Keeping them separate means a temporary DB blip pulls the pod from rotation without killing it, and a deadlocked goroutine can still be caught by the cheap liveness check.
Setting the Content-Type header
Always set Content-Type: application/json before writing the status code.
In Go's net/http, headers must be set before the first call to
w.WriteHeader() or they are silently discarded. Put w.Header().Set(...) first.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Registering a /health route with http.HandleFunc and implementing db.Ping()
- Returning 200 healthy vs 503 unhealthy based on real dependency checks
- Rebuilding a Go binary and restarting the server after editing source
FAQ
How do I add a health check endpoint in Go?
Register an http.HandleFunc for /health, call db.Ping() inside the handler, write a 503 with {"status":"unhealthy"} if Ping returns an error, and a 200 with {"status":"healthy"} if it succeeds. Set Content-Type: application/json before calling WriteHeader.
What is the difference between a liveness probe and a readiness probe in Go?
A liveness probe (/livez) checks that the process is alive - it returns 200 immediately with no dependencies checked. A readiness probe (/readyz) checks that the service can actually handle requests - it runs db.Ping() or similar checks. Kubernetes restarts on liveness failure but only removes the pod from load balancer rotation on readiness failure.
Should a Go health check endpoint always check the database?
Yes, if the database is required for the service to function. A handler that always returns 200 without checking dependencies is misleading - load balancers will route traffic to a pod that cannot serve it. Check every hard dependency (DB, cache, required upstream) in the readiness probe.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →