Uptime Monitoring for Vert.x Applications
Your Vert.x application is running in production — but is it actually healthy? Vert.x is a toolkit, not a framework — it gives you complete control and zero opinions about health endpoints or monitoring. That makes it extremely flexible, but it also means nothing watches your app unless you build it in.
One silent 503 at 3 AM, a periodic verticle that stopped firing after an event loop error, an event bus address that lost its consumer because of a hot-redeploy gone wrong — these fail without making noise. By the end of this guide you'll have external uptime monitoring, multi-region checks, heartbeat monitoring for your periodic verticles, and a public status page — all on the free tier.
Why Vert.x apps still go dark
Vert.x's event-driven, non-blocking model creates failure modes that traditional monitoring misses:
Endpoint failures — your HTTP verticle is running but a handler throws an exception inside routingContext.response().end(). The event loop continues. The JVM stays alive. No process-level alert fires. Callers get connection resets or hanging requests.
Silent periodic verticle failures — a vertx.setPeriodic() callback throws an unchecked exception. Vert.x cancels the timer. No log line is guaranteed. Your recurring data-processing task simply stops, and nothing knows.
Event bus consumer loss — a verticle that handles critical event bus messages is undeployed due to an exception during hot-reload. Producers still publish to the address; messages queue or fail silently. No monitor catches this from outside.
External monitoring catches the first class of failures. Heartbeat monitoring catches the second. Together they give you coverage without requiring Vert.x to become opinionated about observability.
Step 1: Add a health endpoint to your Vert.x server
Vert.x doesn't ship a health module, but adding one takes fewer than 20 lines. Add the Vert.x Web dependency:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.5.10</version>
</dependency>
Create a health verticle that checks your dependencies and exposes /health:
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Pool;
public class HealthVerticle extends AbstractVerticle {
private final Pool dbPool;
public HealthVerticle(Pool dbPool) {
this.dbPool = dbPool;
}
@Override
public void start() {
Router router = Router.router(vertx);
router.get("/health").handler(this::healthHandler);
vertx.createHttpServer()
.requestHandler(router)
.listen(8081); // separate port keeps health off your main API
}
private void healthHandler(RoutingContext ctx) {
checkDatabase()
.onSuccess(dbUp -> {
JsonObject body = new JsonObject()
.put("status", dbUp ? "UP" : "DOWN")
.put("components", new JsonObject()
.put("database", new JsonObject()
.put("status", dbUp ? "UP" : "DOWN")));
int statusCode = dbUp ? 200 : 503;
ctx.response()
.setStatusCode(statusCode)
.putHeader("content-type", "application/json")
.end(body.encode());
})
.onFailure(err -> ctx.response()
.setStatusCode(503)
.putHeader("content-type", "application/json")
.end(new JsonObject()
.put("status", "DOWN")
.put("error", err.getMessage())
.encode()));
}
private Future<Boolean> checkDatabase() {
return dbPool.query("SELECT 1")
.execute()
.map(rows -> true)
.otherwise(false);
}
}
Deploy the health verticle alongside your main verticle:
public class MainVerticle extends AbstractVerticle {
@Override
public void start() {
Pool dbPool = createDbPool();
// Deploy health check on port 8081
vertx.deployVerticle(new HealthVerticle(dbPool))
.onFailure(err -> System.err.println("Health verticle failed: " + err.getMessage()));
// Deploy main API on port 8080
vertx.deployVerticle(new ApiVerticle(dbPool));
}
}
Verify locally:
curl -s http://localhost:8081/health | jq .
Expected output when healthy:
{
"status": "UP",
"components": {
"database": {
"status": "UP"
}
}
}
Step 2: Add external dependency checks
For Redis, external APIs, or message brokers, add additional async checks and run them concurrently using Future.all():
private void healthHandler(RoutingContext ctx) {
Future<Boolean> dbFuture = checkDatabase();
Future<Boolean> redisFuture = checkRedis();
Future<Boolean> externalApiFuture = checkExternalApi();
Future.all(dbFuture, redisFuture, externalApiFuture)
.onComplete(ar -> {
boolean dbUp = dbFuture.result() == Boolean.TRUE;
boolean redisUp = redisFuture.result() == Boolean.TRUE;
boolean apiUp = externalApiFuture.result() == Boolean.TRUE;
boolean allUp = dbUp && redisUp && apiUp;
JsonObject components = new JsonObject()
.put("database", new JsonObject().put("status", dbUp ? "UP" : "DOWN"))
.put("redis", new JsonObject().put("status", redisUp ? "UP" : "DOWN"))
.put("external-api", new JsonObject().put("status", apiUp ? "UP" : "DOWN"));
ctx.response()
.setStatusCode(allUp ? 200 : 503)
.putHeader("content-type", "application/json")
.end(new JsonObject()
.put("status", allUp ? "UP" : "DOWN")
.put("components", components)
.encode());
});
}
All checks run concurrently on the event loop. Response time is bounded by your slowest dependency, not the sum.
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 immediately.
Layer monitors for different parts of your stack:
| Endpoint | What it catches |
|---|---|
| /health | All dependency checks (DB, Redis, external APIs) |
| /api/v1/ping | Main API route availability |
| / | Static asset serving, CDN |
Step 4: Heartbeat monitoring for periodic verticles
HTTP monitors can't observe what happens inside your event loop. For periodic verticles and vertx.setPeriodic() tasks, use heartbeat monitoring.
The pattern: at the end of each successful periodic task, ping a unique URL. No ping = task threw an exception or was cancelled.
Create a heartbeat utility:
import io.vertx.core.Vertx;
import io.vertx.ext.web.client.WebClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Heartbeat {
private static final Logger log = LoggerFactory.getLogger(Heartbeat.class);
private final WebClient client;
private final String url;
public Heartbeat(Vertx vertx, String url) {
this.client = WebClient.create(vertx);
this.url = url;
}
public void ping(String taskName) {
if (url == null || url.isBlank()) {
return;
}
client.getAbs(url)
.send()
.onSuccess(res -> log.debug("Heartbeat sent for {}", taskName))
.onFailure(err -> log.warn("Heartbeat ping failed for {}: {}", taskName, err.getMessage()));
// Non-blocking — fire and forget; failure never crashes the calling task
}
}
Use it in a periodic verticle:
import io.vertx.core.AbstractVerticle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSyncVerticle extends AbstractVerticle {
private static final Logger log = LoggerFactory.getLogger(DataSyncVerticle.class);
private static final long INTERVAL_MS = 60 * 60 * 1000L; // 1 hour
private DataSyncService syncService;
private Heartbeat heartbeat;
@Override
public void start() {
String heartbeatUrl = config().getString("heartbeat.datasync.url", "");
heartbeat = new Heartbeat(vertx, heartbeatUrl);
syncService = new DataSyncService(vertx);
vertx.setPeriodic(INTERVAL_MS, id -> runSync());
}
private void runSync() {
syncService.sync()
.onSuccess(result -> {
log.info("Data sync completed: {} records", result);
// Only ping on success — failure skips the heartbeat
heartbeat.ping("data-sync");
})
.onFailure(err -> {
log.error("Data sync failed: {}", err.getMessage(), err);
// Do NOT ping — let Vigilmon detect the missed beat
// Note: vertx.setPeriodic continues even after exceptions here,
// but missing the heartbeat still alerts you to the failure
});
}
}
Pass configuration from your launcher:
JsonObject config = new JsonObject()
.put("heartbeat.datasync.url", System.getenv().getOrDefault("HEARTBEAT_DATASYNC_URL", ""));
vertx.deployVerticle(new DataSyncVerticle(),
new DeploymentOptions().setConfig(config));
In Vigilmon:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. 70 minutes for an hourly task — 10-minute grace window)
- Copy the unique ping URL
- Set
HEARTBEAT_DATASYNC_URLin your production environment
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
For a reactive toolkit like Vert.x, where failures are often silent by design, fast alerting is especially important. Set your notification channel to alert immediately on the first failure.
Add an uptime badge to your README:
[](https://vigilmon.online?utm_source=devto&utm_medium=article&utm_campaign=vertx-tutorial)
What you've built
| What | How |
|---|---|
| External health checks | /health verticle + Vigilmon HTTP monitor |
| Concurrent dependency checks | Future.all() across DB, Redis, external APIs |
| Silent periodic task detection | Heartbeat ping after successful vertx.setPeriodic() 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 verticle returns accurate 503s when dependencies fail, Vigilmon probes it from multiple regions, and your periodic verticles ping in on every successful run. The next silent event loop failure gets caught before your users do.
Monitor your Vert.x app free at vigilmon.online — monitors running in under a minute, no credit card required.