Go health check endpoint with net/http
Write it yourself: an http.HandlerFunc that runs db.PingContext under a one-second context.WithTimeout and writes http.StatusServiceUnavailable when the ping fails.
There is no health check package in the standard library and you do not need one. The endpoint is a handler, a context with a deadline and one call to the dependency you cannot serve without. Fourteen lines, no framework, no interfaces to satisfy.
The version most services ship writes 200 and returns. It stays green as long as the process is scheduled, which means it stays green through an exhausted pool, a Postgres failover and a deploy that came up with the wrong DSN. db.PingContext fixes that in one line: database/sql either hands back a live connection from the pool or opens a new one, and both fail when the database is genuinely gone. Write http.StatusServiceUnavailable on that path and the response finally carries information. Logdash treats every status outside 200 to 399 as down, so the 503 is what becomes a notification instead of a data point.
The handler
// mux.HandleFunc("GET /readyz", healthHandler(db))
func healthHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "db unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
} Why each line is there
- r.Context() as the parent means a client that hangs up cancels the ping instead of leaving it running against a database that is already struggling.
- One second is the ceiling. The Logdash pinger abandons a request after 10 seconds and records it as down, and an endpoint that regularly needs seconds is telling you something before any monitor does.
- db.PingContext returns an error, not a bool, and it is the only call in database/sql that verifies a connection without pretending to do work.
- http.Error writes a short plaintext body. Do not put err.Error() in there: a driver error can contain the host, the port and the user.
- sql.DB is a pool, so set SetMaxOpenConns. Without a limit, the check can be the request that exhausts the database under load.
- One ping, nothing else. Do not loop over every downstream service, because you will page yourself for someone else.
Point a monitor at it
- 1 Ship the route Deploy with GET /readyz mapped and check it from a machine outside your VPC. If it only answers on the internal listener, an external monitor cannot use it.
- 2 Add the monitor Create a Logdash project and paste the URL. Status code and response time are recorded on every check: every 5 minutes on the free plan, every minute on Builder, every 15 seconds on Pro.
- 3 Kill the database Stop the container while the binary keeps running. PingContext returns an error, the handler writes 503, the monitor goes red and a Telegram message arrives with the endpoint and the status code.
Liveness and readiness are two routes
On Kubernetes, split them. /livez returns 200 whenever the process is running and is what the kubelet restarts on, so it must not touch the database or a slow query will trigger a restart loop during an incident that has nothing to do with your code. /readyz does the ping and is what takes the pod out of the load balancer. Point the external monitor at /readyz, or at a real user-facing route that reads from the database anyway. A monitor aimed at a liveness probe reports green for a pod that is running and serving nothing, which is the same lie as the empty 200 handler, just with more YAML in front of it.
How do I write a health check endpoint in Go?
Register a handler on your ServeMux, derive a context with a one-second timeout from r.Context(), call db.PingContext, and write http.StatusServiceUnavailable on error and 200 otherwise.
What path should a Go health endpoint use?
/healthz is the convention, or /livez and /readyz if you want the Kubernetes split. The path matters far less than whether the handler touches your database before it answers.
Should the health endpoint check the database?
The readiness one should. A process can be alive with an unusable connection pool, and a check that never touches the pool reports 200 straight through that outage.
Do I need a health check library in Go?
No. Fourteen lines of net/http covers it. Libraries help once you have a dozen dependencies to aggregate and want per-check timeouts and caching, and not before.