Skip to content

Custom Health Indicators — Complete Implementation Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Custom Health Indicators. We cover key concepts, practical examples, and best practices to help you master this topic.

Custom health indicators extend built-in health checking frameworks with application-specific logic, enabling verification of business-critical components like external APIs, license servers, and custom data pipelines.

What You'll Learn

By the end of this tutorial, you will know how to create custom health indicators in multiple frameworks, implement complex health logic, and integrate custom indicators with aggregation systems.

Why It Matters

Built-in health indicators only cover generic infrastructure. Real applications have custom dependencies that need specific health checks. Custom indicators fill this gap without reinventing the framework.

Real-World Use

Durga Antivirus Pro has a custom health indicator that checks the virus definition update timestamp. If definitions are older than 24 hours, the service reports degraded status.

Custom Health Indicators Learning Path

flowchart LR
  A[Kubernetes Probes] --> B[Custom Health Indicators]
  B --> C[Spring Boot]
  B --> D[Node.js]
  B --> E[Django]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Spring Boot Custom Health Indicator

Implement the HealthIndicator interface for Spring Boot applications.

@Component
public class VirusDefinitionHealthIndicator implements HealthIndicator {

    private final VirusDefinitionService definitionService;

    public VirusDefinitionHealthIndicator(VirusDefinitionService definitionService) {
        this.definitionService = definitionService;
    }

    @Override
    public Health health() {
        try {
            var lastUpdate = definitionService.getLastUpdateTimestamp();
            var age = System.currentTimeMillis() - lastUpdate;
            var hoursOld = age / 3600000;

            if (hoursOld > 24) {
                return Health.down()
                    .withDetail("service", "virus-definitions")
                    .withDetail("lastUpdate", new Date(lastUpdate).toString())
                    .withDetail("hoursOld", hoursOld)
                    .withDetail("message", "Definitions expired (> 24 hours old)")
                    .build();
            }

            if (hoursOld > 12) {
                return Health.status("DEGRADED")
                    .withDetail("service", "virus-definitions")
                    .withDetail("lastUpdate", new Date(lastUpdate).toString())
                    .withDetail("hoursOld", hoursOld)
                    .build();
            }

            return Health.up()
                .withDetail("service", "virus-definitions")
                .withDetail("lastUpdate", new Date(lastUpdate).toString())
                .withDetail("hoursOld", hoursOld)
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("service", "virus-definitions")
                .build();
        }
    }
}

Node.js Custom Health Indicator

A class-based health indicator pattern for Node.js applications.

class CustomHealthIndicator {
  constructor(name) {
    this.name = name;
    this.lastCheck = null;
    this.cachedResult = null;
  }

  async check() {
    throw new Error("Subclasses must implement check()");
  }

  async getHealth() {
    try {
      const result = await this.check();
      this.lastCheck = Date.now();
      this.cachedResult = {
        status: "UP",
        name: this.name,
        details: result,
        timestamp: new Date().toISOString()
      };
    } catch (err) {
      this.cachedResult = {
        status: "DOWN",
        name: this.name,
        error: err.message,
        timestamp: new Date().toISOString()
      };
    }
    return this.cachedResult;
  }
}

class LicenseServerHealthIndicator extends CustomHealthIndicator {
  constructor() {
    super("license-server");
  }

  async check() {
    const response = await fetch("https://license.dodatech.com/status", {
      signal: AbortSignal.timeout(3000)
    });

    if (!response.ok) {
      throw new Error(`License server returned ${response.status}`);
    }

    const data = await response.json();
    const daysUntilExpiry = data.daysUntilExpiry;

    if (daysUntilExpiry < 7) {
      return {
        healthy: true,
        warning: `License expires in ${daysUntilExpiry} days`,
        daysUntilExpiry,
        status: "DEGRADED"
      };
    }

    return {
      healthy: true,
      daysUntilExpiry,
      status: "UP"
    };
  }
}

const licenseCheck = new LicenseServerHealthIndicator();
licenseCheck.getHealth().then(r => console.log("License:", r.status));

Django Custom Health Indicator

Create custom checks using django-health-check's BaseHealthCheckBackend.

# checks.py
from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import HealthCheckException
from datetime import datetime, timedelta
import requests

class LicenseHealthCheck(BaseHealthCheckBackend):
    critical_service = True

    def check_status(self):
        try:
            response = requests.get(
                "https://license.dodatech.com/status",
                timeout=5
            )

            if response.status_code != 200:
                raise HealthCheckException(
                    f"License server returned {response.status_code}"
                )

            data = response.json()
            days_until_expiry = data.get("days_until_expiry", 0)

            if days_until_expiry <= 0:
                raise HealthCheckException("License has expired")

            if days_until_expiry < 30:
                self.add_warning(f"License expires in {days_until_expiry} days")

        except requests.ConnectionError:
            raise HealthCheckException("Cannot connect to license server")
        except requests.Timeout:
            raise HealthCheckException("License server health check timed out")

    def identifier(self):
        return "license-server"

# Register in settings.py:
# HEALTH_CHECK_BACKENDS = [
#     "myapp.checks.LicenseHealthCheck",
# ]

Express.js Custom Health Middleware

Custom health check middleware for Express.js applications.

function createCustomHealthRouter(checkers) {
  const express = require("express");
  const router = express.Router();

  router.get("/healthz", (req, res) => {
    res.json({ status: "UP", service: "api" });
  });

  router.get("/readyz", async (req, res) => {
    const results = await Promise.allSettled(
      checkers.map(checker => checker.check())
    );

    const details = {};
    let allReady = true;

    checkers.forEach((checker, index) => {
      const result = results[index];
      if (result.status === "fulfilled") {
        details[checker.name] = { status: "UP", ...result.value };
      } else {
        details[checker.name] = { status: "DOWN", error: result.reason.message };
        allReady = false;
      }
    });

    res.status(allReady ? 200 : 503).json({
      status: allReady ? "UP" : "DOWN",
      details
    });
  });

  return router;
}

const licenseChecker = {
  name: "license-server",
  async check() {
    const resp = await fetch("https://license.dodatech.com/status");
    if (!resp.ok) throw new Error("License server unhealthy");
    return { valid: true };
  }
};

const dbChecker = {
  name: "database",
  async check() {
    return { connected: true };
  }
};

const router = createCustomHealthRouter([licenseChecker, dbChecker]);

Health Indicator with Caching

Cache health check results to reduce load on dependency services.

class CachedHealthIndicator {
  constructor(checker, options = {}) {
    this.checker = checker;
    this.cacheTTL = options.cacheTTL || 15000;
    this.cached = null;
    this.lastFetch = 0;
  }

  async getHealth() {
    const now = Date.now();

    if (this.cached && (now - this.lastFetch < this.cacheTTL)) {
      return this.cached;
    }

    try {
      const result = await this.checker();
      this.cached = { status: "UP", details: result, cached: false };
      this.lastFetch = now;
    } catch (err) {
      this.cached = {
        status: "DOWN",
        error: err.message,
        cached: false
      };
      this.lastFetch = now;
    }

    return this.cached;
  }
}

const cachedCheck = new CachedHealthIndicator(
  async () => {
    const resp = await fetch("https://api.example.com/health");
    return { apiStatus: resp.status };
  },
  { cacheTTL: 30000 }
);

cachedCheck.getHealth().then(r => console.log("Cached check:", r.status));

Common Mistakes

  1. Throwing exceptions instead of returning DOWN -- Custom indicators should catch all exceptions and return a structured DOWN response. An unhandled exception may crash the health endpoint.

  2. Making indicators too slow -- A custom indicator that takes 10 seconds delays the entire health response. Set timeouts on all external calls.

  3. Not caching expensive checks -- If a check queries a slow external API, cache the result for a few seconds to avoid overwhelming the dependency.

  4. Ignoring the indicator lifecycle -- Spring Boot indicators should be stateless or reconnect automatically. Don't hold stale connections.

  5. Not testing edge cases -- Test what happens when the dependency returns unexpected data, times out, or returns a non-JSON response.

Practice Questions

  1. How do you create a custom health indicator in Spring Boot? Implement the HealthIndicator interface, override the health() method, and register as a @Component.

  2. Why would you cache health indicator results? To reduce load on dependency services and prevent the health check itself from causing issues during high traffic.

  3. What is the difference between UP, DOWN, and DEGRADED status? UP means fully functional. DOWN means not functional. DEGRADED means functional but with warnings (e.g., near-expiry license, high latency).

  4. Challenge: Implement a custom health indicator that checks multiple sub-services and reports aggregate status with individual details.

class CompositeHealthIndicator {
  constructor(name, indicators) {
    this.name = name;
    this.indicators = indicators;
  }

  async check() {
    const results = await Promise.allSettled(
      this.indicators.map(ind => ind.check())
    );

    const details = {};
    let allUp = true;

    this.indicators.forEach((ind, i) => {
      const r = results[i];
      details[ind.name] = r.status === "fulfilled"
        ? { status: "UP", ...r.value }
        : { status: "DOWN", error: r.reason.message };
      if (r.status !== "fulfilled") allUp = false;
    });

    return {
      name: this.name,
      status: allUp ? "UP" : "DOWN",
      details
    };
  }
}

FAQ

Can custom health indicators have side effects?

No. Health indicators should be read-only. Writing to a database or modifying state in a health check is an anti-pattern.

How many custom health indicators should I create?

One per logical dependency or concern. Don't create a separate indicator for every minor check. Group related checks.

Should custom indicators run on every Kubernetes probe?

For readiness, yes. For liveness, only check process-level health. Custom indicators that check external services belong in readiness.

How do I test custom health indicators?

Mock the dependency and verify the indicator returns UP, DOWN, and DEGRADED statuses correctly. Test timeout and error scenarios.

Can I use environment variables in custom indicators?

Yes. Environment variables are appropriate for configuration values like URLs and timeouts.

Mini Project

Build a custom health indicator library that supports caching, Composite checks, and multiple status levels (UP, DOWN, DEGRADED), with examples for license server, external API, and data freshness checks.

class HealthIndicatorRegistry {
  constructor() {
    this.indicators = [];
  }

  register(indicator) {
    this.indicators.push(indicator);
  }

  async checkAll() {
    const results = {};
    let overall = "UP";

    for (const ind of this.indicators) {
      const result = await ind.getHealth();
      results[ind.name] = result;
      if (result.status === "DOWN") overall = "DOWN";
      if (result.status === "DEGRADED" && overall !== "DOWN") overall = "DEGRADED";
    }

    return { status: overall, checks: results, timestamp: new Date().toISOString() };
  }
}

const registry = new HealthIndicatorRegistry();
registry.register(new LicenseServerHealthIndicator());
registry.checkAll().then(r => console.log("Overall:", r.status));

What's Next

Now that you understand custom health indicators, learn about health check aggregation for combining multiple service health statuses. Then explore monitoring and alerting.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro