AnnouncementsIntroducing Arkoren

    Menu

    Health and Readiness

    Control traffic with cached dependency checks while keeping restart and startup decisions separate.

    What's on this page
    1. Select required dependency checks and distinguish traffic readiness from liveness and startup completion.
    2. Tune non-overlapping background passes while accounting for cooperative cancellation and stale results.
    3. Inspect cached reports safely and run fresh checks without changing the traffic verdict.
    4. Interpret health-transition events without treating them as every possible cause of unavailability.
    On this page

    A database outage can make requests fail while the service process remains healthy enough to recover. Use readiness to control traffic, liveness for restart decisions, and a startup probe to give initialization time to finish. Restarting every replica does not repair an unavailable shared database.

    This page assumes an application coordinator with dependency startup and cleanup already registered. Add checks and routes during synchronous composition, before startup begins. Import application from arkoren.dev/core/application.

    Select Required Checks

    Include a dependency when requests cannot succeed without it. Leave optional integrations out: otherwise their shared outage can remove every replica from traffic. For an existing root connection *postgresdatabase.Driver from arkoren.dev/driver/postgres/postgresdatabase, register its connectivity check:

    composer.Check("inventory-database", connection.Ping)

    Registration does not call Ping. After composition and all startup hooks succeed, a background loop begins checking. Start does not wait for its first result. Nil means passing; an error or panic means failing. A failed check changes readiness, not startup success, liveness, or runner-failure supervision. The loop continues so a later successful pass can restore traffic.

    Check names must be nonempty and application-wide unique; callbacks must be nonnil. Invalid registration panics with ErrNilFunc or ErrDuplicateCheck. Register only during synchronous composition, not in startup, requests, or later goroutines; invalid phases panic with ErrInvalidPhase. Custom network checks must propagate their context and close response bodies. In-memory dependencies need no connectivity check.

    Mount Probes

    Probe handlers have no built-in authentication. Mount them on a listener with restricted network access that your platform can reach. Kubernetes HTTP probes normally connect to the pod IP; a different port does not make a loopback-only listener reachable. Keep sensitive diagnostics on loopback with port forwarding or behind authentication and authorization instead.

    With app := composer.App() and an existing routes *router.Router from arkoren.dev/core/http/router, mount the three application-owned handlers:

    routes.Get(application.ReadyPath, app.ReadyHandler())
    routes.Get(application.LivePath, app.LiveHandler())
    routes.Get(application.StartupPath, app.StartupHandler())

    The paths are /readyz, /livez, and /startupz. Each returns 200 with ok\n or 503 with unavailable\n. None calls a dependency on request; polling readiness more often does not cause more database calls.

    Probe Success Means Platform Use
    Readiness The application is live and its cached checks pass and remain fresh. Without checks, readiness equals liveness. Admit traffic.
    Liveness The application is running, no runner failure is recorded, and the runner group has not finished. Decide whether to restart.
    Startup Startup completed, without waiting for checks. Protect initialization from premature restarts.

    Before startup completes, all return 503. Shutdown makes liveness and readiness unavailable immediately. The startup probe stays successful during and after shutdown following successful startup, if its listener remains reachable. No verdict keeps a stopped listener reachable. A database-check failure alone does not change liveness, while a runner failure does even if cached checks pass. Configure your platform’s health probes accordingly; never use readiness as a restarting probe.

    If you include probes in OpenAPI, pass application.ReadyOperation, LiveOperation, or StartupOperation as the corresponding route’s third argument. These annotations describe responses; they do not mount handlers, enable details, or apply access control.

    Tune Check Timing

    With checks registered, readiness remains pending until the first completed passing pass. A failure makes it unavailable; a later passing pass restores it without restarting. A cached result remains fresh through three intervals of age. Older results make readiness unavailable even if their individual checks passed. Results interrupted by shutdown are discarded.

    The first pass starts immediately after startup. Later passes follow an interval ticker, not a full interval after completion. A 400 ms pass on a one-second interval normally starts its next pass one second after the first began, not 1.4 seconds later. Checks in a pass run concurrently; the pass waits for all of them, and background passes never overlap. Slow callbacks delay later observations.

    Keep the defaults unless dependency latency or your traffic-removal budget needs different timing. The interval defaults to five seconds; the timeout defaults to two fifths of the configured interval, or two seconds by default. To use a shorter cadence, set both during construction (time is the standard library package):

    app := application.New(func(app *application.Builder) {
    	app.ReadinessInterval(2 * time.Second)
    	app.ReadinessTimeout(500 * time.Millisecond)
    })

    Zero or negative selects the respective default. Keep the timeout shorter than the interval. These settings are not setters on a running application.

    Timeouts cancel the pass context, not callback code. A check that ignores cancellation prevents completion and delays later passes and shutdown. Keep checks lightweight, honor cancellation, and return an error when canceled. Returning nil after the deadline is not automatically converted into failure.

    Inspect Cached Results

    When traffic stops reaching an instance, request /readyz?verbose on the restricted probe listener first. It returns JSON with the plain probe’s status and no new dependency calls. Presence of verbose selects JSON regardless of its value, including false. A failed check normally appears as:

    {
      "status": "unavailable",
      "checked_at": "2026-11-01T09:00:00Z",
      "checks": {
        "inventory-database": {
          "status": "failed",
          "error": "reason withheld"
        }
      }
    }

    The timestamp identifies the completed observation, not the request time. Reports also explain not live, no health pass completed, and health result stale. A stale report can still show passing checks; inspect the overall status and reason. Plain and verbose probes set Cache-Control: no-store and X-Content-Type-Options: nosniff.

    Keep HealthDetails disabled unless every affected endpoint is operator-protected. For an operator-restricted deployment, enable details during construction:

    app := application.New(func(app *application.Builder) {
    	app.HealthDetails(true)
    })

    This includes backend error text, which can reveal addresses and connection details. Names remain visible when details are disabled. Actual errors are logged on failure transitions regardless of this setting; protect those logs too.

    The setting also governs cached and fresh MCP health reports, healthy results, and check details in fail-fast condition failures. It does not redact trusted in-process Checks, RunChecks, or CheckHealth results, or event payloads. Protect all affected remote endpoints before enabling it.

    Inside the process, inspect cached typed records without contacting dependencies. With the existing app and the standard library fmt package:

    for _, status := range app.Checks() {
    	fmt.Println(status.Name, status.Err == nil, status.CheckedAt)
    }

    Checks returns copied records in registration order; error values remain shared. It returns nil before the first completed pass and when no checks exist. CheckedAt is the latest pass’s completion time; Since records the first observation or latest passing/failing transition. Use application diagnosis for remote investigation, and the ReadyHandler contract for report edge cases.

    Run Fresh Checks

    An admin command or test may need the dependency’s current state rather than the cached traffic verdict. With an already started app and a caller context ctx carrying a deadline:

    err := app.CheckHealth(ctx)

    Nil means every registered check passed. Otherwise the error joins failed checks in registration order, each prefixed with health and its name. This is the command’s dependency verdict, not an application startup error. Use app.RunChecks(ctx) if you need individual passing and failing CheckStatus records; their Since is zero because a fresh run has no transition history.

    Both methods run checks concurrently with the configured readiness timeout and the caller’s deadline, and wait for every callback to return. They start no dependencies, update no readiness cache, and publish no transition events. Calling before composition cannot see checks not yet registered. A fresh success does not restore traffic; the next passing background observation does.

    Test callbacks directly for their behavior. For wiring tests, compose and start the application before fresh checks and arrange shutdown even if assertions fail. For cached transitions, use testing/synctest rather than fixed sleeps: a fresh check assertion does not exercise the readiness cache.

    Observe Transitions

    Background logs record failure and recovery transitions, not every repeated verdict. CheckFailed fires for a first failing observation or a change from passing to failing; CheckRecovered fires after a failing check passes. Ready and Unready report the first completed pass’s verdict and later changes to whether all checks pass. These events describe background observations, independently of lifecycle liveness.

    Staleness, lifecycle changes, and explicit fresh checks publish none of these events, so events alone cannot explain every 503. Health payloads are trusted in-process data; send errors only to authorized destinations, and do not mutate shared payload slices. Listener failures are logged without changing lifecycle success. Choose observers rather than hooks when alerts must not control startup or shutdown.

    For check records, probe handlers, and settings, see the application API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive