How the Uptime Kuma Heartbeat System Tracks Monitor Status: A Technical Deep Dive

Uptime Kuma creates a database record called a "heartbeat" every time a monitor reports a check result, then uses retry logic, state transition flags, and real-time WebSocket emissions to track and broadcast monitor status.

The Uptime Kuma heartbeat system is the core pipeline that transforms raw check results into persistent status history, uptime statistics, and real-time notifications. Each heartbeat is a lightweight SQLite row that captures the monitor ID, timestamp, response time, and state. This article walks through the complete lifecycle of a heartbeat—from creation in server/model/heartbeat.js to persistence and broadcast—based on the actual source code in the louislam/uptime-kuma repository.

What Is a Heartbeat in Uptime Kuma?

A heartbeat is a RedBean model that maps directly to the heartbeat table. It stores four possible status values defined as constants in server/model/heartbeat.js:

// server/model/heartbeat.js
exports.DOWN          = 0;
exports.UP            = 1;
exports.PENDING       = 2;
exports.MAINTENANCE   = 3;

The model provides two serialization methods. toJSON() returns the full internal representation used for notifications, while toPublicJSON() strips sensitive fields like the message body for public API consumption:

// server/model/heartbeat.js – toPublicJSON()
return {
    status: this.status,
    time:   this.time,
    msg:    "",            // hidden from public view
    ping:   this.ping,
};

How Heartbeats Are Created

Uptime Kuma generates heartbeats through two primary pathways: push-based monitors that report via HTTP and scheduler-based monitors that run internal checks.

Push-Based Monitors

When an external service pushes a status update to /api/push/:pushToken, the router in server/routers/api-router.js dispenses a new heartbeat bean:

// server/routers/api-router.js (excerpt)
let bean = R.dispense("heartbeat");
bean.time        = R.isoDateTimeMillis(dayjs.utc());
bean.monitor_id  = monitor.id;
bean.ping        = ping;               // optional
bean.msg         = msg;
bean.downCount   = previousHeartbeat?.downCount || 0;

If a previous heartbeat exists, the system calculates the duration between checks:

if (previousHeartbeat) {
    isFirstBeat = false;
    bean.duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}

Scheduler-Based Monitors

For HTTP, TCP, and other protocol monitors, each monitor type implements a check() method defined in server/monitor-types/monitor-type.js. The method receives a mutable heartbeat object that it populates with status and timing data:

/**
 * Monitor-type interface
 * @param {Monitor} monitor
 * @param {Heartbeat} heartbeat   // mutable object that will become a bean later
 */
async check(monitor, heartbeat, server) { … }

After the check completes, the scheduler converts this object into a RedBean bean and persists it through the same pipeline used by push monitors.

Status Determination and Retry Logic

Raw status values from monitors undergo transformation through determineStatus() in server/routers/api-router.js. This function applies retry logic, handles "upside-down" monitors (where DOWN means UP), and determines whether a check should be marked as PENDING while retries remain available:

// server/routers/api-router.js – status calculation
determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries,
                monitor.isUpsideDown(), bean);

The function returns a final status of UP, DOWN, or PENDING based on the monitor's configuration and previous state.

Marking Important State Transitions

Not every heartbeat triggers a notification. Uptime Kuma uses Monitor.isImportantBeat() in server/model/monitor.js to identify state transitions that matter for UI updates and alerting:

bean.important = Monitor.isImportantBeat(isFirstBeat,
                                         previousHeartbeat?.status,
                                         bean.status);

The logic considers a beat important when:

  • It is the first beat for a monitor
  • The status transitions between UP and DOWN
  • The status moves into or out of MAINTENANCE
  • A PENDING check finally fails (PENDING → DOWN)
// server/model/monitor.js – isImportantBeat()
return (
    isFirstBeat ||
    (previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
    (previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
    (previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
    (previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
    (previousBeatStatus === UP && currentBeatStatus === DOWN) ||
    (previousBeatStatus === DOWN && currentBeatStatus === UP) ||
    (previousBeatStatus === PENDING && currentBeatStatus === DOWN)
);

A parallel method, isImportantForNotification(), applies additional filters before sending alerts.

Persistence and Real-Time Distribution

Once processed, the heartbeat bean persists to SQLite via RedBean:

await R.store(bean);

Simultaneously, the UptimeCalculator aggregates statistics by updating minutely, hourly, and daily uptime slices:

let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitor.id);
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);

Finally, the server broadcasts the new heartbeat to connected clients via WebSocket:

io.to(monitor.user_id).emit("heartbeat", bean.toJSON());

Front-end dashboards receive this event instantly, updating status indicators without requiring page refreshes.

Accessing Heartbeat Data via API

Uptime Kuma exposes heartbeat data through several endpoints. The single latest heartbeat is available at /api/monitor/:id/heartbeat, returning the full JSON representation.

For public status pages, the endpoint /api/status-page/heartbeat/:slug returns an array of sanitized heartbeat objects using toPublicJSON():

// server/routers/status-page-router.js – building the public array
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());

This ensures sensitive message content remains hidden while allowing external widgets to display current status and response times.

Summary

  • Heartbeat Structure: A lightweight database row storing monitor ID, timestamp, status (0-3), ping duration, and metadata, defined in server/model/heartbeat.js.
  • Creation Paths: Push monitors generate heartbeats via server/routers/api-router.js, while scheduled monitors populate heartbeat objects through the check() interface in server/monitor-types/monitor-type.js.
  • Status Logic: The determineStatus() function applies retry policies and upside-down logic before finalizing the heartbeat state.
  • Importance Filtering: Monitor.isImportantBeat() identifies state transitions requiring UI updates or notifications, filtering out redundant beats.
  • Persistence & Broadcast: RedBean stores beans to SQLite, UptimeCalculator aggregates statistics, and WebSocket events push real-time updates to clients.

Frequently Asked Questions

How does Uptime Kuma handle retry logic for failed checks?

Uptime Kuma implements retry logic in the determineStatus() function within server/routers/api-router.js. When a check fails, the system compares the failure count against the monitor's maxretries configuration. If retries remain, the heartbeat is marked as PENDING (status 2) rather than DOWN, allowing the monitor to attempt additional checks before triggering alerts.

What is the difference between toJSON() and toPublicJSON() in the Heartbeat model?

The toJSON() method returns the complete internal representation of a heartbeat, including the message field (msg) that may contain sensitive error details or response bodies. In contrast, toPublicJSON()—defined in server/model/heartbeat.js—returns a sanitized subset containing only status, time, and ping, with the message field explicitly set to an empty string to prevent data leakage through public APIs.

How does Uptime Kuma calculate uptime percentages from heartbeat data?

Uptime statistics are calculated by the UptimeCalculator class in server/uptime-calculator.js. Each time a heartbeat is stored, the calculator updates time-sliced aggregates using getMinutelyKey(), getHourlyKey(), and getDailyKey() functions to bucket heartbeats by timestamp. The system tracks the ratio of UP beats to total beats within each time window, allowing the dashboard to display precise uptime percentages for arbitrary date ranges without scanning the entire heartbeat history.

Why do some heartbeats get marked as "important" while others do not?

The Monitor.isImportantBeat() method in server/model/monitor.js flags heartbeats as important only when they represent significant state changes. A beat is considered important if it is the first check for a monitor, transitions between UP and DOWN, enters or exits MAINTENANCE mode, or represents a final failure after PENDING retries. This filtering prevents notification spam from consecutive identical states while ensuring critical transitions trigger alerts and UI updates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →