seolcu notes

Uptime Kuma push monitors: 24.8-day interval limit and the always-DOWN query string

Uptime Kuma push intervals cannot exceed 2147483 s (~24.8 days, Node.js 32-bit timer). Monitor longer cadences with a weekly re-push of the last result. Also push to the bare /api/push/<token> URL: pasting the UI’s ?status=up&msg=OK&ping= URL and adding your own status makes every heartbeat record as DOWN.

Status
working
Verified
(0 days before this build)
How
Observed and fixed on a self-hosted Uptime Kuma 2.x (Docker louislam/uptime-kuma:2) with ~30 monitors in production.
Applies to
  • node.js any (setTimeout limit is 2^31-1 ms)
  • uptime kuma 2.x (Docker image louislam/uptime-kuma:2)

Summary

ProblemCauseFix
A job that runs monthly can’t be watched by a push monitorInterval max is 2147483 s (~24.8 days)Weekly re-push of the last recorded result
Interval set above the maxsetTimeout overflow: TimeoutOverflowWarning, check fires every 1 msKeep intervals ≤ 2073600 s (24 days)
Every push records DOWN even when the job succeededQuery string pasted from the UI plus your own status= sends status twiceStore the bare URL ending at the token
Monitor red on the dashboard, nobody alertedMonitor has no notification attachedAudit monitor_notification

1. The interval ceiling

Node.js timers use a signed 32-bit millisecond value: floor(2147483647 / 1000) = 2147483 seconds ≈ 24.8 days. A push monitor with a larger interval emits TimeoutOverflowWarning and evaluates every 1 ms. Leave a margin and use at most 2073600 s (24 days).

Monitoring jobs that run less often than every 24 days

A “first Sunday of the month” job runs 28–35 days apart, so every gap exceeds the ceiling. Raising the interval is not possible. Feed the monitor indirectly:

  1. On success, the job touches a stamp file (for example /var/lib/<x>/update/<service>.stamp) and pushes up. On failure it pushes down and leaves the stamp alone.
  2. A separate weekly timer re-pushes the verdict: up if the stamp is younger than the longest legitimate gap (35 days = 840 h), otherwise down. It also pushes down if the stamp is missing.
  3. Set the monitor interval to 24 days.

The two pushers cannot contradict each other. A failed run leaves the stamp unadvanced, so the next weekly re-push is also down.

#!/usr/bin/env bash
# weekly re-push; KUMA_PUSH_URL comes from an env file kept out of git
set -Eeuo pipefail
stamp=/var/lib/homelab/update/bookstack.stamp
max_h=840
if [[ -e $stamp ]] && (( ( $(date +%s) - $(stat -c %Y "$stamp") ) / 3600 <= max_h )); then
  status=up; msg=last_update_ok
else
  status=down; msg=update_stale_or_missing
fi
curl -fsS -G --data-urlencode "status=$status" --data-urlencode "msg=$msg" "$KUMA_PUSH_URL" >/dev/null

2. Every heartbeat records as DOWN

The UI shows the push URL as https://<kuma>/api/push/<TOKEN>?status=up&msg=OK&ping=. If you store that whole string and your script appends its own status=..., the request carries status twice. Kuma’s handler then compares an array with "up", the comparison fails, and every push is recorded as DOWN, including the successful ones.

Store only https://<kuma>/api/push/<TOKEN> and add parameters with curl -G --data-urlencode.

HTTP 200 from the push endpoint only means the token was accepted, not that the heartbeat was recorded as UP. Check the state that was actually recorded:

sudo python3 -c "
import sqlite3
c=sqlite3.connect('file:/path/to/data/kuma.db?mode=ro',uri=True)
for n,s,m in c.execute('''SELECT m.name,h.status,h.msg FROM monitor m
  LEFT JOIN heartbeat h ON h.id=(SELECT id FROM heartbeat WHERE monitor_id=m.id
  ORDER BY time DESC LIMIT 1) WHERE m.active=1 ORDER BY m.name'''):
    print(('UP' if s==1 else 'DOWN').ljust(6), n, m or '')"

3. Monitors without a notification

Marking a notification as “Default” only applies to monitors created after that. Existing monitors stay silent. In one incident, nine monitors (including every backup heartbeat) had no notification and stayed mute through an outage. Audit them; the query should return zero rows:

sudo docker exec uptime-kuma sqlite3 /app/data/kuma.db \
  "select m.id, m.name from monitor m
   where m.active = 1
     and not exists (select 1 from monitor_notification mn where mn.monitor_id = m.id);"

Not working

Search terms

  • TimeoutOverflowWarning
  • push monitor interval
  • /api/push/
  • status=up&msg=OK&ping=
  • heartbeat always down

Written and verified by Gyuwon Seol (seolcu). If this page solved your problem, you can support the work.

Published 2026-11-09 · last changed 2026-11-09 · Markdown source