# How Maintenance Windows Are Implemented in Uptime Kuma and How They Affect Monitoring

> Learn how Uptime Kuma maintenance windows silence alerts and exclude downtime from uptime calculations. Discover database-backed scheduling and cron job implementation for effective monitoring management.

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

---

**Maintenance windows in Uptime Kuma are database-backed entities scheduled via cron jobs that force a MAINTENANCE status on linked monitors, silencing alerts and excluding the period from uptime calculations.**

Uptime Kuma implements maintenance windows as a first-class scheduling system that transparently pauses monitoring logic during planned downtime. This article examines the four-layer architecture—data model, scheduling engine, monitor linkage, and heartbeat interception—based on the source code in the `louislam/uptime-kuma` repository.

## Architecture of Maintenance Windows

The implementation spans four tightly-coupled layers that handle persistence, timing, association, and runtime impact.

### Data Model and Persistence

The `Maintenance` class in [`server/model/maintenance.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/maintenance.js) extends `BeanModel` and defines the schema for maintenance windows. It stores the title, description, date ranges, recurrence strategies, and active state. Key methods include `toPublicJSON()` for serialization and `generateCron()` for translating recurrence rules into cron expressions.

The model uses RedBean for ORM operations, persisting data to SQLite (or the configured database) and maintaining a global cache in `UptimeKumaServer.maintenanceList`.

### Scheduling Engine

Once a maintenance record is created, the `run()` method (lines 31-48 in [`server/model/maintenance.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/maintenance.js)) instantiates a cron job using the `croner` package. For single occurrences, it schedules a one-time job; for recurring strategies (`recurring-interval`, `recurring-weekday`, `recurring-day-of-month`), it generates a cron expression and creates a repeating job.

When the cron job fires, it updates `beanMeta.status` to `under-maintenance`, clears the API cache (`apicache.clear()`), and broadcasts the updated list via `UptimeKumaServer.sendMaintenanceListByUserID()`.

### Monitor Linkage

Maintenance windows are associated with monitors through many-to-many relationships. The `monitor_maintenance` table links maintenance IDs to monitor IDs, while `maintenance_status_page` links them to status pages.

The socket handler in [`server/socket-handlers/maintenance-socket-handler.js`](https://github.com/louislam/uptime-kuma/blob/main/server/socket-handlers/maintenance-socket-handler.js) provides CRUD operations for these associations. When a user adds monitors to a maintenance window, the handler deletes old links and inserts new rows into `monitor_maintenance`, ensuring the linkage is immediately active for the next heartbeat.

### Heartbeat Interception and Status Override

The critical impact occurs in [`server/routers/api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/server/routers/api-router.js). When a monitor heartbeat arrives at the `/api/push/` endpoint, the code checks `Monitor.isUnderMaintenance(monitor.id)` (defined in [`server/model/monitor.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/monitor.js) lines 30-45).

This method retrieves the linked maintenance IDs, loads each `Maintenance` instance from `UptimeKumaServer.maintenanceList`, and calls `maintenance.isUnderMaintenance()`. If any return true, the heartbeat bean is forced to status `MAINTENANCE` instead of the usual `UP`, `DOWN`, or `PENDING`.

## Lifecycle of a Maintenance Window

Understanding the flow from creation to completion clarifies how the system maintains consistency across the UI, API, and monitoring logic.

### Creation and Persistence

When a user submits the form in [`src/pages/EditMaintenance.vue`](https://github.com/louislam/uptime-kuma/blob/main/src/pages/EditMaintenance.vue), the frontend emits `addMaintenance` via Socket.IO. The server handler in [`maintenance-socket-handler.js`](https://github.com/louislam/uptime-kuma/blob/main/maintenance-socket-handler.js) instantiates a new `Maintenance` bean, stores it via RedBean, and registers it in the global `maintenanceList`. Immediately after storage, `bean.run()` is invoked to schedule the cron job.

### Scheduling and Activation

The `run()` method evaluates the `strategy` field. For a single window, it calculates the delay to the start time and schedules a one-shot cron job. For recurring windows, it generates a cron expression (e.g., `0 2 * * 1` for weekly Monday 2 AM) and creates a repeating job.

When the scheduled time arrives, the callback updates the maintenance status to `under-maintenance`, clears the API response cache, and broadcasts the state change to all connected clients via `sendMaintenanceListByUserID`.

### Heartbeat Processing Under Maintenance

During the maintenance window, every incoming heartbeat for linked monitors triggers the check in [`api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/api-router.js). The `Monitor.isUnderMaintenance()` call iterates through the maintenance IDs associated with that monitor, checks if the current time falls within the window, and returns true if active.

The router then sets `bean.status = MAINTENANCE`. This status is treated specially by the uptime calculator: it is excluded from availability percentages and does not trigger notification rules.

### UI Reflection and Completion

The frontend receives the broadcasted maintenance list in [`src/mixins/socket.js`](https://github.com/louislam/uptime-kuma/blob/main/src/mixins/socket.js) (line 169). [`ManageMaintenance.vue`](https://github.com/louislam/uptime-kuma/blob/main/ManageMaintenance.vue) renders the list with edit controls, while [`StatusPage.vue`](https://github.com/louislam/uptime-kuma/blob/main/StatusPage.vue) (lines 402-425) displays a prominent banner with class `bg-maintenance` when an active window affects the displayed monitors.

When the maintenance window ends (either by cron job completion or manual cancellation), the status transitions to `ended` or `scheduled`, the cron job is stopped, and the UI removes the banner.

## Effect on Monitoring Metrics

Maintenance windows alter three critical aspects of monitoring: uptime calculation, alerting, and public status page display.

### Uptime Calculation Exclusion

The uptime calculator processes heartbeat history to generate availability percentages. When it encounters a heartbeat with status `MAINTENANCE`, it treats the interval as ignored. This means the time spent in maintenance does not count as downtime, nor does it count as uptime; it is effectively removed from the denominator of the availability calculation.

### Alert Suppression

Notification logic resides in `Monitor.sendNotification` and the status determination flow. When a heartbeat is marked `MAINTENANCE`, the code bypasses the usual `DOWN` state detection. Consequently, email, webhook, Discord, and other notification channels are not triggered, preventing alert fatigue during planned work.

### Status Page Rendering

Public status pages query the maintenance status via the API. The `StatusPage` model in [`server/model/status_page.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/status_page.js) (line 573) checks `isUnderMaintenance` for each displayed monitor. If active, the page renders a maintenance banner (using the `bg-maintenance` CSS class) and displays "Under maintenance" text instead of the current operational status, keeping subscribers informed of planned work.

## Code Examples

### Creating a Maintenance Window via Socket API

```javascript
// Client-side Vue component
socket.emit('addMaintenance', {
  title: 'Database upgrade',
  description: 'Planned DB upgrade, expect downtime',
  strategy: 'single',
  dateRange: ['2026-03-15T02:00:00Z', '2026-03-15T04:00:00Z'],
  timeRange: ['02:00', '04:00'],
  active: true,
  monitors: [12, 34]  // IDs of monitors to silence
}, callback);

```

Server handler in [`server/socket-handlers/maintenance-socket-handler.js`](https://github.com/louislam/uptime-kuma/blob/main/server/socket-handlers/maintenance-socket-handler.js):

```javascript
socket.on('addMaintenance', async (maintenance, callback) => {
    const bean = await Maintenance.jsonToBean(R.dispense('maintenance'), maintenance);
    const maintenanceID = await R.store(bean);
    server.maintenanceList[maintenanceID] = bean;
    bean.run();  // schedule the cron job
    callback && callback({ success: true });
});

```

### Linking Monitors to a Maintenance

```javascript
// After creating maintenance, associate monitors
socket.emit('addMonitorMaintenance', maintenanceID, [12, 34], callback);

```

The handler in [`maintenance-socket-handler.js`](https://github.com/louislam/uptime-kuma/blob/main/maintenance-socket-handler.js) (lines 76-92) manages the `monitor_maintenance` many-to-many table.

### Heartbeat Processing with Maintenance Check

In [`server/routers/api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/server/routers/api-router.js) (push endpoint):

```javascript
if (await Monitor.isUnderMaintenance(monitor.id)) {
    msg = 'Monitor under maintenance';
    bean.status = MAINTENANCE;  // forces maintenance status
} else {
    determineStatus(...);
}

```

The check logic in [`server/model/monitor.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/monitor.js):

```javascript
static async isUnderMaintenance(monitorID) {
    // Retrieves linked maintenance IDs from monitor_maintenance table
    const maintenanceIDs = await R.getCol(...);
    for (const id of maintenanceIDs) {
        const maintenance = UptimeKumaServer.maintenanceList[id];
        if (maintenance && maintenance.isUnderMaintenance()) {
            return true;
        }
    }
    return false;
}

```

### Frontend Display

In [`src/pages/StatusPage.vue`](https://github.com/louislam/uptime-kuma/blob/main/src/pages/StatusPage.vue) (lines 402-425):

```html
<div v-if="maintenanceList.length > 0" class="alert bg-maintenance">
  <h4 class="alert-heading">{{ maintenance.title }}</h4>
  <div v-html="maintenanceHTML(maintenance.description)"></div>
  <MaintenanceTime :maintenance="maintenance" />
</div>

```

## Summary

- **Database-First Design**: Maintenance windows are persistent entities stored via RedBean in [`server/model/maintenance.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/maintenance.js), supporting single and recurring schedules.
- **Cron-Based Scheduling**: The `croner` package executes start/stop callbacks that update status and broadcast state changes via `UptimeKumaServer.sendMaintenanceListByUserID`.
- **Monitor Linkage**: Many-to-many relationships via `monitor_maintenance` and `maintenance_status_page` tables associate windows with specific monitors and public status pages.
- **Heartbeat Interception**: The `/api/push/` endpoint checks `Monitor.isUnderMaintenance()` and forces a `MAINTENANCE` status, bypassing outage detection and notifications.
- **Metric Exclusion**: Periods marked as maintenance are excluded from uptime calculations and prevent false alerts, while status pages display a `bg-maintenance` banner to inform users.

## Frequently Asked Questions

### How do I create a recurring maintenance window in Uptime Kuma?

Use the [`EditMaintenance.vue`](https://github.com/louislam/uptime-kuma/blob/main/EditMaintenance.vue) interface or emit the `addMaintenance` socket event with a `strategy` of `recurring-interval`, `recurring-weekday`, or `recurring-day-of-month`. The `generateCron()` method in [`server/model/maintenance.js`](https://github.com/louislam/uptime-kuma/blob/main/server/model/maintenance.js) converts these strategies into cron expressions, and the `croner` package schedules the recurring execution. Ensure you specify the `monitors` array to link the window to specific monitor IDs.

### Why don't I receive alerts during a maintenance window?

When a monitor is linked to an active maintenance window, the heartbeat processing logic in [`server/routers/api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/server/routers/api-router.js) detects this via `Monitor.isUnderMaintenance()` and sets the heartbeat status to `MAINTENANCE` instead of `DOWN`. The notification dispatcher skips alerts for this status, preventing alert fatigue during planned outages. The uptime calculator also ignores these intervals, ensuring they do not affect availability metrics.

### How does Uptime Kuma display maintenance windows on public status pages?

The [`StatusPage.vue`](https://github.com/louislam/uptime-kuma/blob/main/StatusPage.vue) component subscribes to the `maintenanceList` socket event broadcast from `UptimeKumaServer.sendMaintenanceListByUserID`. When rendering, it checks if any linked maintenance is active via the `monitor_maintenance` associations. Active windows trigger a banner with the `bg-maintenance` CSS class and display "Under maintenance" text instead of the standard operational status, keeping subscribers informed of scheduled work.

### What happens if a monitor heartbeat arrives exactly when a maintenance window starts?

The `croner` job created by `maintenance.run()` executes at the scheduled start time and immediately updates `beanMeta.status` to `under-maintenance`. Concurrent heartbeats processed by [`api-router.js`](https://github.com/louislam/uptime-kuma/blob/main/api-router.js) call `Monitor.isUnderMaintenance()`, which loads the maintenance instance from `UptimeKumaServer.maintenanceList` and checks the current status. If the cron job has fired, the maintenance is active, and the heartbeat receives the `MAINTENANCE` status, ensuring no gap exists between the scheduled start and monitoring suppression.