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:
- Sign up at app.uptimesignal.io -- free tier includes 25 monitors
- Add your health endpoint -- enter
https://api.example.com/actuator/healthas the URL - Set the check interval -- 5 minutes (free) or 1 minute (Pro) depending on your SLA requirements
- 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-authorizedin 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