Application Health Monitoring
Monitors a custom health endpoint that reports your application's internal status. Unlike uptime checks that only verify HTTP availability, application health monitoring checks the status of internal components like your database, cache, queue, and external services.
How It Works
Hoppla calls your health endpoint at the configured interval and evaluates the JSON response. The endpoint follows the Oh Dear health-check format: it returns a finishedAt timestamp plus a checkResults array, where each entry describes one internal component (database, cache, queue, external service, …). Hoppla aggregates the individual check statuses into one overall result.
Expected Endpoint Format
Your health endpoint should return a JSON response with a finishedAt timestamp and a checkResults array. Each entry has name, status, and optional notificationMessage, shortSummary, and meta fields:
{
"finishedAt": "2026-06-01T12:00:00+00:00",
"checkResults": [
{
"name": "Database",
"status": "ok",
"notificationMessage": "Database connection healthy",
"shortSummary": "OK",
"meta": { "connection_time_ms": 4 }
},
{
"name": "Queue",
"status": "warning",
"notificationMessage": "Queue backlog growing",
"shortSummary": "1,204 jobs",
"meta": { "pending": 1204 }
}
]
}Status Values
Each check in checkResults reports one of the following per-check status values:
status | Meaning | Effect |
|---|---|---|
ok | Component is healthy | Passes |
warning | Component is degraded but functional | Overall warning |
failed | Component check failed | Overall incident |
crashed | Component check threw / could not run | Overall incident |
skipped | Check was skipped | Treated as passing |
Hoppla computes the overall status from the individual checks: fail (incident) if any check is failed or crashed, warning if any check is warning (or the finishedAt timestamp is stale), otherwise ok.
Configuration
| Setting | Description | Default |
|---|---|---|
| Health endpoint path | Relative path appended to the monitor URL (absolute URLs are rejected for SSRF safety) | health |
| Health check secret | Sent to your endpoint as the oh-dear-health-check-secret request header | (none) |
| Timeout | Maximum time to wait for the response (seconds, 1–60) | 10 |
| Stale threshold | Warn if finishedAt is older than this many minutes | 5 |
| Check interval | How often to poll the health endpoint | 5 minutes |
The health endpoint is always a relative path appended to the monitor URL — for example, path health on monitor https://example.com resolves to https://example.com/health. Absolute URLs are not accepted.
Results
| Field | Description |
|---|---|
| finished_at | The finishedAt timestamp reported by your endpoint |
| checks | The parsed individual checks (name, status, message, short summary, meta) |
| checks_total | Number of checks reported |
| checks_failed | Number of checks that failed or crashed |
| checks_warning | Number of checks in a warning state |
Example Result
Health check: https://example.com/health (200 OK)
finished_at: 2026-06-01T12:00:00+00:00
checks_total: 2
checks_failed: 0
checks_warning: 1
Checks:
Database: ok
Queue: warning (1,204 jobs) ← flaggedAlerts
| Event | Trigger | Severity |
|---|---|---|
incident.created | Any check reports failed/crashed, the endpoint returns a 5xx, or the request fails entirely | Incident |
warning.application_health | Any check reports warning, the finishedAt timestamp is stale, the endpoint returns 401/403 (auth misconfigured) or 404/501 (not configured), or the body is not valid JSON | Warning |
A 5xx response (or a transport-level failure) opens an incident. A 401 or 403 indicates the secret is misconfigured, and a 404 or 501 indicates the endpoint is not set up — both produce a warning rather than an incident, as does a non-JSON response body or a stale finishedAt. The warning event type is warning.application_health.
Implementation Examples
Laravel
Route::get('/health', function () {
$checks = [
['name' => 'Database', 'status' => rescue(fn () => DB::connection()->getPdo() ? 'ok' : 'failed', 'crashed')],
['name' => 'Cache', 'status' => rescue(fn () => Cache::store()->get('health-check') !== false ? 'ok' : 'failed', 'crashed')],
['name' => 'Queue', 'status' => rescue(fn () => Queue::size('default') >= 0 ? 'ok' : 'failed', 'crashed')],
];
return response()->json([
'finishedAt' => now()->toIso8601String(),
'checkResults' => $checks,
]);
});Express (Node.js)
app.get('/health', async (req, res) => {
const checkResults = [
{ name: 'Database', status: (await checkDatabase()) ? 'ok' : 'failed' },
{ name: 'Cache', status: (await checkRedis()) ? 'ok' : 'failed' },
{ name: 'Queue', status: (await checkQueue()) ? 'ok' : 'failed' },
];
res.json({
finishedAt: new Date().toISOString(),
checkResults,
});
});Best Practices
- Keep the health endpoint lightweight -- avoid expensive queries or external calls that could time out
- Use timeouts on each component check to prevent the health endpoint itself from hanging
- Protect the endpoint with the health check secret — Hoppla sends it as the
oh-dear-health-check-secretheader, so your endpoint can reject requests that lack it - Return a check per subsystem (one
checkResultsentry each) so you can quickly identify which component is failing