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

> Discover how Uptime Kuma heartbeat tracks monitor status using database records, retry logic, state flags, and real-time WebSocket broadcasts. Learn the technical details.

- Repository: [Louis Lam/uptime-kuma](https://github.com/louislam/uptime-kuma)
- Tags: deep-dive
- Published: 2026-02-28

---

**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`](https://github.com/louislam/uptime-kuma/blob/main/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`](https://github.com/louislam/uptime-kuma/blob/main/server/model/heartbeat.js):

```javascript
// 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:

```javascript
// 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`](https://github.com/louislam/uptime-kuma/blob/main/server/routers/api-router.js) dispenses a new heartbeat bean:

```javascript
// 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:

```javascript
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`](https://github.com/louislam/uptime-kuma/blob/main/server/monitor-types/monitor-type.js). The method receives a mutable heartbeat object that it populates with status and timing data:

```javascript
/**
 * 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`](https://github.com/louislam/uptime-kuma/blob/main/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:

```javascript
// 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`](https://github.com/louislam/uptime-kuma/blob/main/server/model/monitor.js) to identify state transitions that matter for UI updates and alerting:

```javascript
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`)

```javascript
// 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:

```javascript
await R.store(bean);

```

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

```javascript
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:

```javascript
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()`:

```javascript
// 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`](https://github.com/louislam/uptime-kuma/blob/main/server/model/heartbeat.js).
- **Creation Paths**: Push monitors generate heartbeats via [`server/routers/api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/server/routers/api-router.js), while scheduled monitors populate heartbeat objects through the `check()` interface in [`server/monitor-types/monitor-type.js`](https://github.com/louislam/uptime-kuma/blob/main/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`](https://github.com/louislam/uptime-kuma/blob/main/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`](https://github.com/louislam/uptime-kuma/blob/main/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`](https://github.com/louislam/uptime-kuma/blob/main/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`](https://github.com/louislam/uptime-kuma/blob/main/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.