Framework Guide

Spring Boot API Monitoring

Set up health check endpoints and uptime monitoring for your Spring Boot application using Actuator, custom HealthIndicators, and UptimeSignal. Examples in Java 21 and Kotlin.

Why Monitor Your Spring Boot API?

Spring Boot applications run on the JVM, which adds its own failure modes on top of typical web application issues. Connection pools exhaust silently, garbage collection pauses freeze request handling, and thread pool saturation makes your app unresponsive without actually crashing.

  • Connection pool exhaustion -- HikariCP connections fill up under load, causing new queries to queue and eventually timeout
  • GC pauses -- Long garbage collection pauses freeze all request processing, returning timeouts to clients
  • Thread pool saturation -- Tomcat's thread pool fills up with slow requests, blocking new connections
  • Dependency failures -- Database, cache, or external service outages cascade through your application context

Adding Spring Boot Actuator

Spring Boot Actuator provides production-ready health checks out of the box. Add the dependency to your project:

Maven (pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Gradle (build.gradle.kts)

implementation("org.springframework.boot:spring-boot-starter-actuator")

Once added, Spring Boot exposes /actuator/health automatically. It returns {"status":"UP"} with HTTP 200 when healthy, and {"status":"DOWN"} with HTTP 503 when any health indicator fails.

Configuring the Health Endpoint

Configure Actuator in application.properties (or application.yml):

# Expose only the health endpoint
management.endpoints.web.exposure.include=health

# Show component details (options: never, when-authorized, always)
management.endpoint.health.show-details=always

# Include specific health indicators
management.health.db.enabled=true
management.health.diskspace.enabled=true
management.health.redis.enabled=true

# Optional: run Actuator on a separate port
# management.server.port=8081

With show-details=always, the health response includes each component:

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "PostgreSQL",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 107374182400,
        "free": 85899345920
      }
    }
  }
}

Custom HealthIndicator (Java)

Implement HealthIndicator to add custom checks for your application's dependencies:

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentClient paymentClient;

    public PaymentGatewayHealthIndicator(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @Override
    public Health health() {
        try {
            var response = paymentClient.ping();
            if (response.isSuccessful()) {
                return Health.up()
                    .withDetail("provider", "stripe")
                    .withDetail("responseTime", response.latencyMs())
                    .build();
            }
            return Health.down()
                .withDetail("provider", "stripe")
                .withDetail("statusCode", response.statusCode())
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("provider", "stripe")
                .build();
        }
    }
}

Custom HealthIndicator (Kotlin)

import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.stereotype.Component

@Component
class PaymentGatewayHealthIndicator(
    private val paymentClient: PaymentClient
) : HealthIndicator {

    override fun health(): Health = try {
        val response = paymentClient.ping()
        if (response.isSuccessful) {
            Health.up()
                .withDetail("provider", "stripe")
                .withDetail("responseTime", response.latencyMs)
                .build()
        } else {
            Health.down()
                .withDetail("provider", "stripe")
                .withDetail("statusCode", response.statusCode)
                .build()
        }
    } catch (e: Exception) {
        Health.down(e)
            .withDetail("provider", "stripe")
            .build()
    }
}

Database Health Checks

Spring Boot auto-configures a DataSourceHealthIndicator when you have spring-boot-starter-data-jpa or spring-boot-starter-jdbc on the classpath. It runs a validation query against your datasource automatically.

For more control, create a custom database health check that validates connection pool state:

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class DatabasePoolHealthIndicator implements HealthIndicator {

    private final HikariDataSource dataSource;

    public DatabasePoolHealthIndicator(HikariDataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        var pool = dataSource.getHikariPoolMXBean();
        int active = pool.getActiveConnections();
        int total = pool.getTotalConnections();
        int waiting = pool.getThreadsAwaitingConnection();

        var builder = Health.up()
            .withDetail("activeConnections", active)
            .withDetail("totalConnections", total)
            .withDetail("threadsWaiting", waiting)
            .withDetail("maxPoolSize", dataSource.getMaximumPoolSize());

        // Flag as degraded if pool is nearly exhausted
        if (active > dataSource.getMaximumPoolSize() * 0.8) {
            return Health.status("DEGRADED")
                .withDetail("activeConnections", active)
                .withDetail("maxPoolSize", dataSource.getMaximumPoolSize())
                .withDetail("warning", "Connection pool near capacity")
                .build();
        }

        return builder.build();
    }
}

Actuator Security Configuration

In production, expose only what you need. The recommended setup keeps /actuator/health public for monitoring while locking down everything else:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                // Health endpoint is public (for UptimeSignal)
                .requestMatchers("/actuator/health").permitAll()
                // All other actuator endpoints require auth
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .build();
    }
}

Alternatively, isolate Actuator on a separate port that is not publicly accessible:

# application.properties
server.port=8080
management.server.port=8081

# Only expose health
management.endpoints.web.exposure.include=health

Setting Up Monitoring with UptimeSignal

Once your Actuator health endpoint is configured, connect it to UptimeSignal for continuous uptime monitoring:

  1. Sign up at app.uptimesignal.io -- free tier includes 25 monitors
  2. Add your health endpoint -- enter https://api.example.com/actuator/health as the URL
  3. Set the check interval -- 5 minutes (free) or 1 minute (Pro) depending on your SLA requirements
  4. Configure alerts -- UptimeSignal sends email alerts when your endpoint returns non-200 or times out

UptimeSignal checks your endpoint externally, which catches issues that internal health checks miss: DNS failures, TLS certificate problems, load balancer misconfigurations, and network-level outages.

Best Practices

  • Keep health checks fast -- Actuator health checks should respond in under 500ms. Add timeouts to external dependency checks
  • Use health groups -- Spring Boot 3.x supports health groups: separate liveness (/actuator/health/liveness) from readiness (/actuator/health/readiness) checks
  • Don't expose sensitive details publicly -- Use show-details=when-authorized in production, or keep the public endpoint minimal
  • Monitor the management port separately -- If Actuator runs on a different port, ensure UptimeSignal can reach it
  • Include connection pool metrics -- HikariCP pool exhaustion is one of the most common Spring Boot production failures

Common Spring Boot Production Issues

HikariCP Pool Exhaustion

Slow queries or missing @Transactional annotations cause connections to leak. The pool fills up, and new requests queue until they timeout. Monitor hikaricp_connections_active and alert when it approaches maximumPoolSize.

Tomcat Thread Pool Saturation

By default, Tomcat allows 200 concurrent threads. If downstream services are slow, threads pile up waiting for responses. Your app accepts no new connections, but the process is still running. External health checks with timeouts catch this.

GC Pauses and Memory Pressure

Full GC pauses on large heaps can freeze your app for seconds. With G1GC or ZGC on Java 21, long pauses are rare but still possible under memory pressure. External monitoring detects the gap that internal metrics miss.

Spring Context Initialization Failures

Misconfigured beans, missing environment variables, or database migration failures can prevent your application from starting. Kubernetes readiness probes and UptimeSignal catch deployments that fail to become healthy.

Frequently Asked Questions

What is the default Spring Boot Actuator health endpoint?
Spring Boot Actuator exposes a health endpoint at /actuator/health by default. It returns HTTP 200 with {"status":"UP"} when healthy and HTTP 503 with {"status":"DOWN"} when any health indicator fails. Add spring-boot-starter-actuator to your dependencies to enable it.
How do I expose health check details in Actuator?
By default, Actuator only shows the aggregate status. Set management.endpoint.health.show-details=always in application.properties to expose component-level details. For production, use when-authorized so only authenticated requests see the full breakdown.
Should I secure Actuator endpoints in production?
Yes. Expose only the health endpoint publicly and lock down everything else. Use management.endpoints.web.exposure.include=health to limit what is available. Consider running Actuator on a separate management port with management.server.port. Keep /actuator/health unauthenticated so external monitoring services like UptimeSignal can reach it.
How do I write a custom HealthIndicator?
Implement the HealthIndicator interface and annotate your class with @Component. Override the health() method and return Health.up().build() or Health.down().withDetail("error", message).build(). Spring Boot automatically discovers and includes it in the /actuator/health response.
How often should I monitor my Spring Boot API health?
For production APIs, check every 1-5 minutes. UptimeSignal's free tier checks every 5 minutes, which catches most outages quickly. Pro tier supports 1-minute intervals for faster detection. Actuator health checks are lightweight and designed to handle frequent polling without impacting application performance.

Start monitoring your Spring Boot API

Get alerted when your Spring Boot endpoints fail. Setup takes 30 seconds.

25 monitors free. No credit card required.

More Framework Guides