Files
spring-boot-demo/actuator-in-production/docs/07-groups-and-probes.md
Ankur Mhatre 958b401f0f Spring Boot startup time: bean-by-bean diagnosis, and one directory per post
Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable
Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup
and FlightRecorderApplicationStartup behind a system property, and a /diag/startup
endpoint that computes step self time -- the number /actuator/startup does not give
you and the one that names the actual culprits.

Captured under docs/output/: the step tree sorted both ways, the same startup as JFR
events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath
scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache
timings (6.93 s to 4.82 s). Post body and metadata live in post/.

Moves the existing Actuator project into actuator-in-production/ so the repository
holds one directory per article; the root README is now an index.
2026-09-05 00:17:37 +05:30

4.7 KiB

← 06 Failure modes · 07 · Groups, probes and Kubernetes · 08 The diagnostics endpoint →

07 — Groups, probes and Kubernetes

The failure this prevents

A third-party API goes down. Your externalApi indicator reports DOWN. /actuator/health aggregates to DOWN and answers 503. Your Kubernetes manifest points both probes at it:

livenessProbe:  { httpGet: { path: /actuator/health, port: 8080 } }
readinessProbe: { httpGet: { path: /actuator/health, port: 8080 } }

The readiness failure is correct — this instance cannot serve. The liveness failure is a catastrophe: the kubelet kills the container, every replica fails the same probe at the same time, and the whole deployment enters a restart loop. A partial outage in someone else's system has become a total outage in yours, and the restarts make recovery slower because every fresh JVM has to warm up while the upstream is still failing.

Liveness answers "is this process broken beyond recovery?" Almost nothing external belongs in it, because restarting cannot fix an external problem.

Readiness answers "should this instance receive traffic right now?" Dependencies belong here.

The configuration

From application-groups.yaml:

management:
  endpoint:
    health:
      group:
        liveness:
          include: livenessState,diskSpace
        readiness:
          include: readinessState,ordersDatabase,externalApi
        startup:
          include: ordersDatabase
      probes:
        enabled: true

Each group gets its own path: /actuator/health/liveness, /actuator/health/readiness, /actuator/health/startup. Groups can carry their own show-details too.

The evidence

output/08-groups-and-probes.txt:

--- baseline: upstream UP ---
  GET /actuator/health              HTTP 503
  GET /actuator/health/liveness     HTTP 200
  GET /actuator/health/readiness    HTTP 200
  GET /actuator/health/startup      HTTP 200

--- upstream DOWN ---
  GET /actuator/health              HTTP 503
  GET /actuator/health/liveness     HTTP 200
  GET /actuator/health/readiness    HTTP 503
  GET /actuator/health/startup      HTTP 200

Liveness stayed 200 across the outage. Readiness moved. Kubernetes removes this instance from the Service endpoints and leaves the process alone; when the upstream recovers, readiness goes green and traffic returns without a single restart.

Note the aggregate /actuator/health was 503 in both columns — the Kafka indicator is down throughout, since this repository runs no broker. That is exactly why you should not point a probe at the aggregate: it is the union of everything, and it tells the orchestrator nothing actionable.

The manifest

livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 9001 }
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 9001 }
  periodSeconds: 5
  failureThreshold: 2
startupProbe:
  httpGet: { path: /actuator/health/startup, port: 9001 }
  periodSeconds: 5
  failureThreshold: 30

Port 9001 is the management port from chapter 04. The kubelet reaches it inside the pod network; the ingress does not.

The startupProbe matters more than people think. Without one, a JVM that takes 45 seconds to warm up under failureThreshold: 3 and periodSeconds: 10 gets killed at 30 seconds, forever, and the symptom is a crash-loop with no error in the logs.

readinessState, and shutting down cleanly

readinessState is Boot's own indicator, driven by ApplicationAvailability. During graceful shutdown Boot flips it to REFUSING_TRAFFIC before the server stops accepting, so readiness goes red while in-flight requests finish. Pair it with:

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

You can also drive it yourself by publishing an AvailabilityChangeEvent — useful for taking an instance out of rotation before a risky migration.

Which dependencies belong in readiness

Ask: if this is down, can this instance still serve any useful request?

  • Primary database → yes, readiness.
  • Kafka, for a service whose only job is consuming → yes, readiness.
  • Kafka, for a service that also serves reads → probably not; degrade rather than withdraw.
  • A recommendations API you fall back to a static list for → no. Do not check it at all.

Every dependency in readiness is a dependency that can take your service out of rotation. That list should be shorter than your instinct suggests.


← 06 · 07 · 08 The diagnostics endpoint →