How to add a custom health probe

A custom health probe monitors an application-specific dependency (a Redis cache, an external API, a feature flag service) and contributes its state to Piko's /live and /ready endpoints. This guide shows how to write and register one. See the health API reference for the surface.

Implement the interface

A probe implements piko.HealthProbe:

package probes

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
    "piko.sh/piko"
)

type RedisProbe struct {
    client *redis.Client
}

func NewRedisProbe(client *redis.Client) *RedisProbe {
    return &RedisProbe{client: client}
}

func (p *RedisProbe) Name() string {
    return "ApplicationRedis"
}

func (p *RedisProbe) Check(ctx context.Context, checkType piko.HealthCheckType) piko.HealthStatus {
    start := time.Now()

    err := p.client.Ping(ctx).Err()
    state := piko.HealthStateHealthy
    message := "redis reachable"
    if err != nil {
        state = piko.HealthStateUnhealthy
        message = fmt.Sprintf("redis ping failed: %v", err)
    }

    return piko.HealthStatus{
        Name:      p.Name(),
        State:     state,
        Message:   message,
        Timestamp: time.Now(),
        Duration:  time.Since(start).String(),
    }
}

The checkType argument distinguishes liveness checks from readiness checks. A liveness check is fast and answers "is the process alive?". A readiness check is thorough and answers "can this component serve traffic right now?". Return quickly for liveness because a ping is usually sufficient. For readiness, an end-to-end operation such as write-then-read, schema check, or auth-token refresh works well.

Distinguish liveness from readiness

func (p *RedisProbe) Check(ctx context.Context, checkType piko.HealthCheckType) piko.HealthStatus {
    if checkType == piko.HealthCheckLiveness {
        return liveness(ctx, p)
    }
    return readiness(ctx, p)
}

Liveness should never fail for transient issues. If it returns Unhealthy, the orchestrator restarts the process. Readiness can fail more eagerly. A Degraded or Unhealthy result tells the orchestrator to stop routing traffic until the probe recovers.

Register the probe

Pass it at bootstrap:

redisClient := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")})

ssr := piko.New(
    piko.WithCustomHealthProbe(probes.NewRedisProbe(redisClient)),
)

The probe contributes to both /live and /ready automatically.

Combine with lifecycle

A component that needs both managed startup/shutdown and health probing can implement both interfaces. See the lifecycle how-to for the combined pattern.

Choose a state value

StateMeaningBehaviour
HealthStateHealthyComponent is working normally.Endpoint returns 200.
HealthStateDegradedComponent is working with reduced performance or limited features.Endpoint returns 200 but surfaces the state in the response body.
HealthStateUnhealthyComponent is not working.Endpoint returns 503.

A single Unhealthy probe marks the overall endpoint as unhealthy. Degraded probes do not. The endpoint stays 200.

Timeouts and context

The ctx argument carries a deadline (default 5 seconds, set via piko.WithHealthCheckTimeout(...)). Honour it. Piko cancels any probe that blocks past the deadline, and the health endpoint reports it as unhealthy with a timeout message.

Expose the endpoint

The health server defaults to 127.0.0.1:9090. To expose it to an orchestrator, set the bind address in func main:

ssr := piko.New(
    piko.WithHealthEnabled(true),
    piko.WithHealthBindAddress("0.0.0.0"),
)

The endpoint reveals internal service state. Do not expose it on a public network without the reverse-proxy controls appropriate for your environment.

See also