Uptime Monitoring for Micronaut Applications
Your Micronaut application is running in production — but is it actually healthy? Micronaut ships with built-in health indicators and a fast compile-time DI model, but a local health endpoint only tells you what your own JVM can observe. You need an independent external monitor that probes from outside your infrastructure and alerts you when something breaks.
One silent 503 at 3 AM, a @Scheduled task that stopped executing after a configuration change, a connection pool that exhausted itself under load — these fail invisibly until a user complains. By the end of this guide you'll have external uptime monitoring, multi-region checks, heartbeat monitoring for your Micronaut scheduled tasks, and a public status page — all on the free tier.
Why Micronaut apps still go dark
Micronaut's lightweight design is its strength — and its monitoring blind spot:
Endpoint failures — your REST layer starts returning 5xx after a bad deploy or a saturated datasource pool. The JVM is healthy. The /health endpoint returns UP because the readiness checks only validate what you explicitly registered. Business endpoints can be broken while health looks fine.
Silent scheduled task failures — a @Scheduled method throws an exception. Micronaut logs a warning and schedules the next invocation. No alert, no incident. Your billing job has been failing for three days.
External monitoring catches both because it observes your app from the user's perspective, not from inside the JVM.
Step 1: Enable Micronaut Health
Add the management dependency to your pom.xml:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
Or with Gradle (build.gradle):
implementation("io.micronaut:micronaut-management")
Micronaut auto-discovers built-in health indicators for datasources, MongoDB, Redis, and others. The /health endpoint is enabled by default. Verify it works locally:
curl -s http://localhost:8080/health | jq .
Expected output:
{
"name": "my-service",
"status": "UP",
"details": {
"jdbc": {
"name": "my-service",
"status": "UP",
"details": {
"datasources": { "status": "UP" }
}
},
"diskSpace": {
"name": "my-service",
"status": "UP",
"details": { "free": 45678901234, "threshold": 10485760 }
}
}
}
Configure the health endpoint in application.yml:
endpoints:
health:
enabled: true
sensitive: false
details-visible: ANONYMOUS
Step 2: Add a custom health indicator
For dependencies that Micronaut doesn't auto-detect, implement HealthIndicator:
import io.micronaut.context.annotation.Requires;
import io.micronaut.health.HealthStatus;
import io.micronaut.management.health.indicator.HealthIndicator;
import io.micronaut.management.health.indicator.HealthResult;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.util.Map;
@Singleton
@Requires(beans = PaymentServiceClient.class)
public class PaymentServiceHealthIndicator implements HealthIndicator {
private final PaymentServiceClient client;
public PaymentServiceHealthIndicator(PaymentServiceClient client) {
this.client = client;
}
@Override
public Publisher<HealthResult> getResult() {
return Mono.fromCallable(() -> {
try {
client.ping();
return HealthResult.builder("payment-service", HealthStatus.UP).build();
} catch (Exception e) {
return HealthResult.builder("payment-service", HealthStatus.DOWN)
.details(Map.of("error", e.getMessage()))
.build();
}
}).onErrorReturn(
HealthResult.builder("payment-service", HealthStatus.DOWN)
.details(Map.of("error", "unreachable"))
.build()
);
}
}
Micronaut collects all registered HealthIndicator beans and combines their results. A single DOWN indicator causes the overall /health response to return HTTP 503.
Step 3: Set up external monitoring with Vigilmon
With /health returning accurate 503s on failure, point Vigilmon at it:
- Sign up at vigilmon.online — free tier, no credit card
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval (5 minutes on free tier)
- Save
Vigilmon probes from multiple geographic regions simultaneously. A non-2xx response or connection timeout opens an incident and alerts you before users find out.
Layer monitors to pinpoint failures quickly:
| Endpoint | What it catches |
|---|---|
| /health | All registered health indicators (DB, cache, external APIs) |
| /health/liveness | JVM hang, deadlock, OOM killer |
| /api/v1/ping | Business route availability independent of health indicators |
| / | Frontend or CDN serving broken |
If you expose liveness and readiness separately, configure them:
endpoints:
health:
liveness:
enabled: true
readiness:
enabled: true
Step 4: Heartbeat monitoring for @Scheduled tasks
HTTP monitors can't observe your application's scheduler. For Micronaut @Scheduled methods, use heartbeat monitoring.
The pattern: ping a unique URL at the end of each successful job execution. Missed ping = failed job = alert.
Create a heartbeat service:
import io.micronaut.context.annotation.Value;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
@Singleton
public class HeartbeatService {
private static final Logger log = LoggerFactory.getLogger(HeartbeatService.class);
private final HttpClient httpClient;
@Value("${heartbeat.url:}")
private String heartbeatUrl;
public HeartbeatService(@Client HttpClient httpClient) {
this.httpClient = httpClient;
}
public void ping(String jobName) {
if (heartbeatUrl == null || heartbeatUrl.isBlank()) {
return;
}
try {
httpClient.toBlocking().exchange(heartbeatUrl);
} catch (Exception e) {
// Log but never rethrow — monitoring outage must not crash the job
log.warn("Heartbeat ping failed for job '{}': {}", jobName, e.getMessage());
}
}
}
Wire it into a @Scheduled method:
import io.micronaut.scheduling.annotation.Scheduled;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class OrderSyncJob {
private static final Logger log = LoggerFactory.getLogger(OrderSyncJob.class);
private final OrderSyncService syncService;
private final HeartbeatService heartbeat;
public OrderSyncJob(OrderSyncService syncService, HeartbeatService heartbeat) {
this.syncService = syncService;
this.heartbeat = heartbeat;
}
@Scheduled(cron = "0 0 1 * * ?")
void run() {
try {
syncService.syncOrders();
// Only ping on success — exception skips the heartbeat
heartbeat.ping("order-sync");
} catch (Exception e) {
log.error("Order sync failed: {}", e.getMessage(), e);
// Do NOT ping — let Vigilmon detect the missed beat and alert
}
}
}
Configure per environment in application.yml:
heartbeat:
url: ${HEARTBEAT_URL:}
In Vigilmon:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. 25 hours for a nightly job — 1-hour grace window)
- Copy the unique ping URL
- Set
HEARTBEAT_URLin your production environment
Now if syncOrders() throws, the heartbeat is skipped, Vigilmon detects the missed ping, and you get an alert.
Step 5: Webhook alerts and the uptime badge
Slack and Discord alerts:
- In Vigilmon go to Notifications → New Channel
- Choose Slack or Discord, paste your webhook URL
- Enable it on your monitors
You'll get an instant alert when a monitor goes down and a recovery notification when it comes back up.
Add an uptime badge to your README:
[](https://vigilmon.online?utm_source=devto&utm_medium=article&utm_campaign=micronaut-tutorial)
What you've built
| What | How |
|---|---|
| External health checks | /health endpoint + Vigilmon HTTP monitor |
| Datasource and cache checks | Micronaut auto-discovered health indicators |
| Custom dependency checks | HealthIndicator bean implementations |
| Silent scheduled task detection | Heartbeat ping after successful @Scheduled run |
| Instant incident alerts | Slack/Discord webhook notifications |
| README status badge | Vigilmon badge embed |
The whole setup runs on Vigilmon's free tier and takes under 30 minutes. Your health endpoint returns accurate 503s when dependencies fail, Vigilmon probes it from multiple regions, and your scheduled jobs ping in on every successful run. The next silent failure gets caught before your users find it.
Monitor your Micronaut app free at vigilmon.online — monitors running in under a minute, no credit card required.