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

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 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) 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 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. When a monitor heartbeat arrives at the /api/push/ endpoint, the code checks Monitor.isUnderMaintenance(monitor.id) (defined in 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, the frontend emits addMaintenance via Socket.IO. The server handler in 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. 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 (line 169). ManageMaintenance.vue renders the list with edit controls, while 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 (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

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

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

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

The handler in 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 (push endpoint):

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:

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 (lines 402-425):

<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, 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 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 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 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 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 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.

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 →