skip to content

How do you make a health group return a custom HTTP status code (e.g. map OUT_OF_SERVICE to 503) independently of the main health endpoint?

level: seniorimportance: should knowfreq 18%

answer

  1. status.http-mapping.<status>=<code>
  2. defaults: DOWN/OUT_OF_SERVICE=503, UP/UNKNOWN=200
  3. per-group overrides global overrides built-in
  4. liveness 200 vs readiness 503 for OUT_OF_SERVICE
  5. status.order = aggregation severity, not HTTP code

basics

~10 s

Use management.endpoint.health.group.<name>.status.http-mapping.<status>=<code>, e.g. map out-of-service to 503. This overrides the global status-to-HTTP mapping for that one group.

solid answer

~30 s

Actuator translates the aggregated `Status` into an HTTP status code via a status-to-HTTP mapping. Defaults: UP/UNKNOWN → 200, DOWN → 503, OUT_OF_SERVICE → 503. You override globally with `management.endpoint.health.status.http-mapping.<status>=<code>`, and per group with `management.endpoint.health.group.<name>.status.http-mapping.<status>=<code>`. This is essential for probes: a readiness group can map OUT_OF_SERVICE → 503 so the load balancer stops routing, while a liveness group keeps 200 for the same state so Kubernetes doesn't restart the pod. Related, `.status.order` overrides the severity ordering that decides which member status 'wins' the aggregation. So one endpoint mechanism gives you both custom aggregation ordering and custom HTTP semantics, tuned per consumer.

code

yaml · 16 lines
yaml
management:
  endpoint:
    health:
      group:
        readiness:
          include: readinessState, db
          status:
            http-mapping:
              out-of-service: 503   # pull from LB rotation
        liveness:
          include: livenessState
          status:
            http-mapping:
              out-of-service: 200   # same state must NOT restart the pod
            # custom severity ordering example:
            order: DOWN, OUT_OF_SERVICE, UP, UNKNOWN

go deeper

for a junior

Know that DOWN maps to 503 by default and it's configurable.

for a middle

Write the per-group http-mapping property and state the defaults.

for a senior

Explain the liveness-200 vs readiness-503 split for OUT_OF_SERVICE and separate mapping from ordering.

for a principal

Introduce a custom Status, set status.order so it wins aggregation, and map it to an HTTP code across probe groups coherently.

## From Status to HTTP code A health response has a body (`{"status":"..."}`) and an HTTP status code. Actuator maps the aggregated `Status` to the code using an `HttpCodeStatusMapper`. The **defaults**: - `UP` → 200 - `UNKNOWN` → 200 - `DOWN` → 503 (Service Unavailable) - `OUT_OF_SERVICE` → 503 Everything else defaults to 200. So a monitoring system can decide health from the HTTP code alone without parsing JSON. ## Overriding the mapping Globally: ``` management.endpoint.health.status.http-mapping.down=500 ``` Per group (scoped to that group only): ``` management.endpoint.health.group.readiness.status.http-mapping.out-of-service=503 management.endpoint.health.group.liveness.status.http-mapping.out-of-service=200 ``` The `<status>` key is the status name (case-insensitive / relaxed binding — `out-of-service`, `OUT_OF_SERVICE` both bind). The value is the HTTP code. ## Why per-group mapping is the point Consider Kubernetes probes and Spring's `ApplicationAvailability`: - **Liveness**: DOWN means the app is broken beyond recovery → Kubernetes should restart. A merely OUT_OF_SERVICE (temporarily paused) state should NOT trigger a restart, so map it to 200 for the liveness group. - **Readiness**: OUT_OF_SERVICE means "don't send me traffic right now" → return 503 so the service is pulled from the load-balancer rotation, but the pod is not killed. Same underlying state, two groups, two HTTP codes — impossible without per-group mapping. ## Status ORDER vs status MAPPING (don't confuse them) Two different knobs: - **`status.http-mapping.<status>`** — maps a final status to an HTTP code (output translation). - **`status.order`** — the severity ranking used when AGGREGATING multiple member statuses into one. Default order (most severe first) is `DOWN, OUT_OF_SERVICE, UP, UNKNOWN`. If you introduce a custom `Status` (e.g. `FATAL`), you set `management.endpoint.health.group.<name>.status.order=FATAL,DOWN,OUT_OF_SERVICE,UP,UNKNOWN` so it wins, then map it: `...http-mapping.fatal=500`. ## Gotchas - Mapping only changes the HTTP code, not the body `status` string — clients reading the JSON still see the real status. - A common mistake is expecting DOWN to be a client-error 4xx; it's 503 (server can't serve), which is correct for load balancers. - Group mapping falls back to the global mapping, which falls back to the built-in defaults — only the keys you specify are overridden; unspecified statuses keep their inherited code. - If you add a custom status but forget `status.order`, aggregation may not treat it as most-severe, so a member returning it can be silently overridden by UP. - Relaxed binding applies to status names, but keep them consistent to avoid confusion.

  • What's the difference between status.http-mapping and status.order?
    http-mapping translates a FINAL aggregated status into an HTTP code (output). order defines the severity ranking used to AGGREGATE multiple member statuses into one final status (input to that final value). They operate at different stages.
  • Why map OUT_OF_SERVICE to 503 for readiness but 200 for liveness?
    OUT_OF_SERVICE means temporarily unable to serve. Readiness returning 503 removes the pod from load-balancing; liveness returning 200 keeps Kubernetes from restarting a pod that is alive and will recover.

saying these in an interview costs you the question

  • Saying DOWN maps to a 4xx by default (it's 503)
  • Conflating status.order (aggregation) with status.http-mapping (HTTP code)
  • Thinking the mapping also changes the JSON body's status string
  • Assuming you must write a custom controller to change health HTTP codes

context