tutorial

Uptime Monitoring for Micronaut Applications

Your Micronaut app is running in production — but is it actually healthy? Set up external uptime monitoring, heartbeat checks for scheduled tasks, and instant alerts in under 30 minutes — free.

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:

  1. Sign up at vigilmon.online — free tier, no credit card
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval (5 minutes on free tier)
  5. 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:

  1. Click New Monitor → Heartbeat
  2. Set the expected interval (e.g. 25 hours for a nightly job — 1-hour grace window)
  3. Copy the unique ping URL
  4. Set HEARTBEAT_URL in 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:

  1. In Vigilmon go to Notifications → New Channel
  2. Choose Slack or Discord, paste your webhook URL
  3. 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:

[![Uptime](https://vigilmon.online/badge/your-monitor-id.svg)](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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →