Self-hosted uptime and status monitoring. Think Uptime Kuma, but built on MariaDB, with multi-location probes, roles and permissions, SSO, long-term rollups and a modern UI.
Copyright (c) PD-Codes. Licensed under GPL-3.0-only. No telemetry, no phone-home.
- Quick start
- Features
- Screenshots
- Configuration reference
- Architecture
- Probes (multi-location checks)
- Working with many monitors
- Status page history and groups
- Certificates and TLS errors
- Single sign-on with SAML (Entra ID)
- Backup and restore
- Upgrading
- Importing from Uptime Kuma
- Config as code
- Security notes
- Development and tests
- Tested limits
- Surprise features
- Known limitations
Requirements: Docker with Compose v2.
cp .env.example .env
# edit .env: set PW_DB_PASSWORD and PW_BASE_URL (and ideally PW_SECRET_KEY)
docker compose up -d --buildOpen http://localhost:3000. The first visit shows the setup page where you create the administrator account.
Want to look around first? Start with sample data (10 monitors, 90 days of history, a status page at /status/demo):
PW_DEMO=true PW_DEMO_PASSWORD=demo-demo-1 docker compose up -d --build
# sign in as admin / demo-demo-1Demo mode seeds once. Do not enable it on a production database.
Behind a reverse proxy, set PW_BASE_URL=https://your.host and PW_TRUST_PROXY=true. The base URL is used for links in mails, OIDC callbacks and passkeys. Server-sent events need response buffering turned off (nginx: proxy_buffering off; for /api/stream).
Useful endpoints: /health (liveness), /ready (database and scheduler), /metrics (Prometheus, see PW_METRICS_TOKEN), /api/docs (OpenAPI/Swagger UI), /api/push/<token> (heartbeats).
Monitoring: HTTP(S) with method/headers/body/auth/status codes/redirects/TLS toggle, keyword and JSON path checks, multi-step HTTP flows, TCP, ICMP ping, UDP, DNS, TLS certificate expiry (with chain), domain/WHOIS expiry, SMTP, IMAP/POP3, SSH banner, SFTP (password or key login, optional pinned host key, file or directory presence, minimum size and maximum age), WebSocket, gRPC, MQTT, SNMP, NTP (clock offset), RADIUS, LDAP, FTP/FTPS, RDP, SIP, RabbitMQ, Kafka, MariaDB/MySQL, PostgreSQL, MSSQL, Oracle, Redis, MongoDB, Docker container state, PM2 processes, Globalping (ping/HTTP from a chosen country or city), game servers (Minecraft, Steam/Source, about 350 more through GameDig, Pelican/Pterodactyl panels, CubeCoders AMP) and push/heartbeat and manual-status monitors. Every monitor has interval, timeout, retries, up/down thresholds, flap detection, tags, groups, pause/clone/bulk edit and notification rules. Failure modes (DNS, refused, timeout, TLS, wrong status, ...) are distinct error kinds in the event log.
Operations: nested groups with dependencies (children are suppressed while a parent is down), automatic and manual incidents with six statuses (open, acknowledged, identified, being fixed, monitoring, resolved), groups as incident scope, assignee, notes, post-mortem and MTTR/MTTA, maintenance windows (once or cron, timezone-aware), status pages (custom domain, branding, custom CSS, password or login protection, email/RSS/webhook subscriptions).
Notifications: email, Telegram, Discord, Slack, Microsoft Teams, ntfy, Gotify, Pushover and templated webhooks (plus a plugin hook, registerChannelType). Templates, escalation policies, quiet hours, per-channel test button and a delivery log with retry.
Security and users: local accounts, OIDC (Entra ID, Keycloak, Google, ...), SAML 2.0 (Entra ID), LDAP, TOTP, passkeys (WebAuthn), API tokens with scopes, session management, audit log, roles plus per-group and per-status-page permissions.
Data: raw heartbeats roll up to minute, hour and day aggregates (exact percentiles per minute); raw data lives in daily partitions that are dropped by retention. Charts stay fast over years.
Reports and integrations: SLA reports as PDF/CSV (also scheduled by mail), daily digest, Prometheus metrics, outgoing webhooks, OpenAPI, Kuma import, YAML/JSON config export and import.
UI: light/dark/system theme, German and English (browser language or profile setting), responsive down to phone width, installable PWA, command palette (Ctrl/Cmd+K), keyboard shortcuts (? lists them), kiosk mode at /kiosk, reduced-motion support.
Captured in demo mode (PW_DEMO=true): 10 sample monitors and 90 days of generated history.
Responsive down to phone width:
Everything is configured through environment variables. Any variable also accepts a _FILE variant that reads the value from a file (Docker secrets), e.g. PW_DB_PASSWORD_FILE=/run/secrets/db.
| Variable | Default | Description |
|---|---|---|
PW_ROLE |
all |
all, api (HTTP only) or worker (scheduler, rollups, notifications only). |
PW_HOST |
0.0.0.0 |
Listen address. |
PW_PORT |
3000 |
Listen port. |
PW_BASE_URL |
http://localhost:3000 |
Public URL. Used for links, OIDC redirects, passkey origin. https:// also turns on secure cookies. |
PW_TRUST_PROXY |
false |
Trust X-Forwarded-* from a reverse proxy (client IPs, rate limits). |
PW_LOG_LEVEL |
info |
pino log level. Logs are JSON on stdout. |
PW_DATA_DIR |
./data (/data in the image) |
Holds the generated secret key when PW_SECRET_KEY is not set. |
PW_WEB_DIR |
./dist/web |
Built UI. |
PW_MIGRATIONS_DIR |
./migrations |
SQL migrations, applied at startup under a database lock. |
| Variable | Default | Description |
|---|---|---|
PW_DB_HOST |
127.0.0.1 |
MariaDB host (mariadb in the compose file). |
PW_DB_PORT |
3306 |
MariaDB port. |
PW_DB_NAME |
pulsewatch |
Database name. |
PW_DB_USER |
pulsewatch |
Database user. |
PW_DB_PASSWORD |
empty | Database password (required by the compose file). |
PW_DB_POOL |
20 |
Connection pool size. |
| Variable | Default | Description |
|---|---|---|
PW_SECRET_KEY |
generated | Master key for encrypting secrets at rest (AES-256-GCM) and signing cookies. If unset, a key file is created in PW_DATA_DIR. Back it up together with the database, encrypted values cannot be recovered without it. Generate one with openssl rand -base64 32. |
PW_SESSION_TTL_HOURS |
168 |
Session lifetime. |
PW_COOKIE_SECURE |
from PW_BASE_URL |
Force the Secure cookie flag. |
PW_SSRF_MODE |
allow-private |
allow-private or public-only. Can be overridden at runtime in Settings. |
PW_SSRF_ALLOW |
empty | Comma separated IPs/CIDRs exempt from public-only. |
PW_SSRF_DENY |
empty | Comma separated IPs/CIDRs that are always blocked. Link-local and metadata ranges are blocked by default. |
PW_RATE_LIMIT_MAX |
300 |
API requests per minute per client. Static assets and health endpoints are exempt. |
PW_RATE_LIMIT_LOGIN |
10 |
Login attempts per minute per client. |
PW_METRICS_TOKEN |
empty | Bearer token for /metrics. Without it only signed-in administrators can read it. |
| Variable | Default | Description |
|---|---|---|
PW_TICK_MS |
1000 |
Scheduler tick. |
PW_MAX_CONCURRENCY |
200 |
Maximum checks running at once. |
PW_JITTER_PCT |
5 |
Random spread of the interval, avoids thundering herds. |
PW_RETENTION_RAW_DAYS |
14 |
Raw heartbeats (dropped by partition). |
PW_RETENTION_MINUTE_DAYS |
60 |
Minute rollups. |
PW_RETENTION_HOUR_DAYS |
400 |
Hour rollups. |
PW_RETENTION_DAY_DAYS |
3650 |
Day rollups. |
PW_RETENTION_EVENT_DAYS |
365 |
Event log. |
PW_RETENTION_AUDIT_DAYS |
730 |
Audit log. |
| Variable | Default | Description |
|---|---|---|
PW_SMTP_HOST / PW_SMTP_PORT |
empty / 587 |
SMTP server for digests, reports and subscription mails. Alert emails use the per-channel SMTP settings. |
PW_SMTP_USER / PW_SMTP_PASS |
empty | SMTP credentials. |
PW_SMTP_FROM |
PulseWatch <pulsewatch@localhost> |
Sender address. |
PW_SMTP_SECURE |
false |
Implicit TLS (port 465). |
PW_DEADMAN_URL |
empty | URL pinged every PW_DEADMAN_INTERVAL seconds while the scheduler is healthy. Point it at a push monitor of another PulseWatch (or any heartbeat service) to be alerted when this instance dies. |
PW_DEADMAN_INTERVAL |
60 |
Seconds between pings. |
PW_OTEL_EXPORTER_OTLP_ENDPOINT |
empty | OTLP/HTTP endpoint (for example http://collector:4318). When set, every monitor check is exported as a span to /v1/traces. |
| Variable | Default | Description |
|---|---|---|
PW_DEMO |
false |
Seed sample data on first start and expose demo target endpoints under /demo/*. |
PW_DEMO_PASSWORD |
random, logged | Password of the demo admin user. |
| Variable | Default | Description |
|---|---|---|
PW_SERVER_URL |
required | Base URL of the PulseWatch server. Must be https:// (see below). |
PW_PROBE_TOKEN |
required | Token shown once when the probe is created. |
PW_PROBE_FETCH_SECONDS |
30 |
How often the job list is refreshed. |
PW_PROBE_CONCURRENCY |
50 |
Maximum parallel checks. |
PW_PROBE_SSRF_MODE / _ALLOW / _DENY |
allow-private |
Same semantics as the server variables, applied on the probe. |
PW_PROBE_ALLOW_INSECURE |
false |
Permit a plain http:// server URL. Only for tests. |
The compose file passes all PW_RETENTION_* variables through. PW_IMAGE, PW_PROBE_IMAGE, PW_PUBLIC_PORT (host port, default 3000) and PW_DB_BUFFER_POOL (MariaDB InnoDB buffer pool, default 256M) only affect the compose file.
browser (React SPA, SSE) ──► app (Fastify API + scheduler + jobs) ──► MariaDB 11
▲
probe agents (pull jobs, push results over HTTPS) ┘
- One process by default.
PW_ROLE=allruns API, scheduler, rollups, report jobs and the notifier. For bigger setups runPW_ROLE=apireplicas behind a load balancer and one or morePW_ROLE=workercontainers (docker compose --profile split up -d). Everything coordinates through MariaDB: due checks are claimed withSELECT ... FOR UPDATE SKIP LOCKEDand a lease, so two workers never run the same check and a crash just lets the lease expire. Redis is not needed. - Scheduler (
src/server/engine): persistednext_run_atper monitor, jitter, concurrency cap, per-check hard timeout, graceful shutdown. A restart neither loses nor duplicates rounds. - Check runners (
src/core/runners): every monitor type is a self-contained runner with a field definition (drives the UI form and server validation) and arun()function. Add a type withregisterRunner. - State machine (
engine/machine.ts): retries (which failures, how many), up threshold, flap detection, dependency suppression, maintenance overlay, multi-location quorum. Pure and unit-tested. - Data (
migrations/): versioned SQL migrations.heartbeatsis partitioned by day;rollupsholds minute, hour and day aggregates; secrets are stored asenc:v1:ciphertext. - Realtime: server-sent events (
/api/stream), the UI patches its query cache, there is no polling. - Notifications (
src/server/notify): event bus, per-monitor rules, quiet hours, escalation policies, delivery queue with retries and a log. - Auth (
src/server/auth): scrypt passwords, TOTP, passkeys, OIDC, LDAP, bearer tokens with scopes, CSRF token bound to the session, audit log. - Frontend (
web/): React 19, Vite, Tailwind, TanStack Query, uPlot. English and German strings live inweb/src/locales. - Why Node.js and not Go? The scheduler is I/O bound (sockets, HTTP, DNS), which Node's event loop handles well: 5,000 monitors at 30 s run at about 165 checks per second on 2 vCPUs, see Tested limits. One language across API, runners, probe and UI keeps the project small. If you ever need hundreds of thousands of monitors, the probe protocol is simple enough to reimplement the agent in Go.
A probe is a small agent (Dockerfile.probe, about 60 MB) that runs checks from another network and reports back. It only makes outbound connections.
-
In the UI open Probes and create a probe. Copy the token, it is shown only once.
-
Run the agent on the remote host:
docker run -d --restart unless-stopped --name pulsewatch-probe \ -e PW_SERVER_URL=https://pulsewatch.example.com \ -e PW_PROBE_TOKEN=pwp_... \ --cap-drop ALL --cap-add NET_RAW --read-only \ pulsewatch/probe:latest
Or with this repository:
PW_PROBE_TOKEN=pwp_... PW_SERVER_URL=https://... docker compose --profile probe up -d probe. -
Edit a monitor, choose its locations (server and/or probes) and set the quorum: the monitor is only down when at least that many locations fail.
Use HTTPS between probe and server. Job payloads contain decrypted monitor credentials so the probe can authenticate against your targets. The agent refuses a plain http:// server URL unless it points to localhost or PW_PROBE_ALLOW_INSECURE=true. Tokens can be rotated or a probe disabled at any time without touching monitors.
Probe results are stored when they arrive. The combined verdict for a monitor is evaluated on its normal schedule (and on "Check now"), so a probe-only monitor reacts within one interval.
- Dashboard: a click anywhere on a card or row opens the monitor. The pause/resume button on each card acts at once (and rolls back if the server refuses). Select shows checkboxes, Select all shown picks everything the current filter lists.
- Bulk add: on New monitor switch on Add several monitors at once, then paste one target per line (
name | address,name; address, a tab-separated pair from a spreadsheet, or just the address). A live preview marks duplicate names; everything else on the form is shared. Lines that fail validation stay in the box so you can fix and resend them. - Bulk edit: timings, retries (how many and which failures), thresholds, flapping, group, public badge, tags (add/remove) and notification rules (add or replace) for all selected monitors. Fields left on keep are not touched.
- Tags: two kinds. Simple tags are just a name (click to attach). Tags with a value have a name and a value (
env: production, the model Uptime Kuma uses); both fields are dropdowns that end with Add new..., which opens a small dialog. A monitor can carry the same name with several values. Chips showname: value, the dashboard search finds values, Groups & tags lists the values in use with their counts, and export/import and the Kuma import keep them (tags: [simple, {name: env, value: production}]in YAML/JSON). - Recent checks filter: the Recent checks card on a monitor has a status dropdown with multiple selection. It lists only the statuses that monitor has produced lately (up, down, pending, maintenance, suppressed) and filters all pages, not just the visible one.
- Latency statistics: the chart and the report use plain names with a tooltip: Typical (p50) is the median, Slow (p95) means 19 of 20 checks were faster, Worst case (p99) means 99 of 100 were faster.
- Retries, timeout and intervals work like in Uptime Kuma. Heartbeat interval is how often a monitor is checked. When a check fails (a timeout and an unreachable host alike), the monitor turns pending and is checked again after the Heartbeat retry interval, up to Retries times; only when the last retry fails it is down and the notification goes out. A success in between starts over silently, and a monitor that is already down stays on the regular interval.
0retries reports a failure right away. Request timeout is how long one check may take. New monitors start like in Kuma: timeout 80% of the interval (48 s at 60 s), retry interval equal to the interval. The form keeps them tied the same way: the retry interval follows the interval while both are equal, and a timeout above 80% of the interval is cut back when you leave the field. It also shows the worst case until a monitor is reported as down, and a pending check says which retry follows. Not part of Kuma and left out: choosing which errors are retried. Different from Kuma: Up threshold (successes in a row before a down monitor is up again, default 1 = like Kuma) and no Resend notification if down X times (escalation policies repeat alerts). This replaces the former pair retries and down threshold; on upgrade the migration folds them into one number so every monitor keeps its timing (down threshold x (retries + 1) - 1, capped at 100), and old exports and saved defaults are converted the same way on import. - Defaults: Settings > Defaults sets the timings, retries and thresholds that pre-fill new monitors and apply when an API call or import omits them. Existing monitors are not changed.
- Inline group: the folder button next to Group creates a (sub)group without leaving the form.
- Incident badge: the menu shows the number of open incidents nobody has acknowledged; the same number prefixes the browser tab title. The top bar has a + button for a new monitor.
Mark a status page as home page (status page list, Use as home page) and it is served at / for everyone. The admin UI stays reachable at /dashboard and /admin, which ask for a login. Without a home page / goes to the dashboard as before. Only one page can be the home page; setting another one moves the mark. A page that is not public is never used. A status page with its own domain is the start page on that domain; /dashboard, /admin and /login still work there.
An incident moves through open, acknowledged, identified, being fixed, monitoring and resolved; any status can follow any other one, and reopening clears the end time. In the incident dialog you can pick a whole monitor group instead of ticking every monitor. One switch decides what visitors see: Show on status pages on the incident (the eye button in Incidents does the same). It is off by default: new incidents stay internal, and so do the incidents PulseWatch opens by itself when a monitor goes down. To publish those right away, turn on Show detected incidents on status pages in Settings > Incidents. An incident that is shown appears on the status pages and the RSS feed, and subscribers get its status changes by email; one that is not shown appears nowhere and nothing is sent. Each timeline entry has its own visibility: status changes and the resolution are always shown, a plain note is internal unless you tick Show on the status page when you add it. Entries written by people can be edited afterwards, text and visibility alike (Edit below the entry; edited entries carry an edited label, also for visitors). Entries PulseWatch writes itself (detected, assigned, acknowledged) cannot be edited, and the public page only says that a post-mortem was written, never what it says. Auto-resolve (the monitor recovered) leaves an incident in monitoring alone until someone resolves it; if the monitor fails again while monitoring, the incident reopens.
In the status page editor an announcement is one click away: pick Info, Maintenance or Incident (a notice with an optional start and end). Subscribers (mail subscriptions) only appears when SMTP is configured. The same rule hides every other function that sends mail (daily digest, scheduled reports) while PW_SMTP_HOST is empty.
Each status page has its own history length (14, 30, 60, 90, 120, 150 or 180 days, default 90). In the editor, Add a whole group adds a monitor group, optionally with its subgroups, as sections; monitors already on the page are skipped.
The Address (slug) of a new status page follows the title as you type (Public Services becomes public-services, umlauts become ae, oe, ue, ss) until you edit it by hand; clearing the field makes it follow the title again.
Past incidents: the public page lists resolved incidents of the last 1, 3, 7, 14, 30 (default), 60, 90, 180 or 365 days, or none at all (switch Show past incidents off). Ongoing incidents always show. Long lists are paged (5, 10, 20 or 50 per page, default 10); visitors can change the page size themselves. The RSS feed follows the same age limit. The settings are part of the configuration export and import.
Language: each status page has a Language setting. English or Deutsch fixes it for the page, its emails, its confirmation pages and its RSS feed. Visitor decides (the default) follows the browser language and shows an EN/DE switch; a link with ?lang=de or ?lang=en opens the page in that language. Incident texts in two languages: detected incidents are worded by the page ("Router is down" / "Router ist nicht erreichbar") in the visitor's language. For incidents, timeline notes, status messages and announcements you write yourself, the forms have a folded block English and German wording (optional); an empty version shows the text as written, so one language is still enough. Visitors, the RSS feed (/rss?lang=de, with a <language> tag), the history and the emails to subscribers use the version in their language: a page with a fixed language always uses that one, otherwise subscribers get the language they signed up in (webhook payloads carry it as lang). The title of an existing incident and its versions are edited under Wording on the status page on the incident, notes through Edit. A manual resolve reads "Resolved manually" / "Manuell behoben". The public endpoints accept ?lang=en|de. English only or German only: Settings > Languages sets one language for the whole instance (default: English and German). With a single language every status page counts as fixed to it (no EN/DE switch, feed, subscriber emails and incident texts follow, a stored version in the other language is simply not used), alerts, the daily digest and report mails and files are written in it whatever the profile says, and the second-wording fields disappear from the forms. The language of the app itself stays a personal choice in the profile.
Game servers: Minecraft (Java and Bedrock) and the other game monitors (Steam/Source, GameDig, Pelican, AMP) report players online, version and, where the server tells, message of the day and player names. The dashboard, the monitor page, the kiosk and the status page show them. Per status page, Game server details chooses: Hidden, Players online, version and message of the day (default) or Also show the names of players online. A server that is down shows no player data.
Settings > Certificates imports private CA certificates (PEM, paste or file). They are trusted in addition to the built-in roots by HTTP, TLS, SMTP, MQTT, database, WebSocket and gRPC checks, on the server and on remote probes (probes receive them with their job list). Expired and duplicate certificates are skipped and reported. Processes reload the store every 30 seconds.
TLS failures now name what happened: expiry date and days, subject, issuer, host:port, SNI and the Node error code, for example TLS certificate expired on 2026-01-02 (12 days ago): CN=..., issuer CN=..., host a.example.com:443 (CERT_HAS_EXPIRED). The same text appears in heartbeats, logs, down mails and the daily digest, which also lists each failing monitor with its target.
- In Entra ID create an Enterprise application (non-gallery), open Single sign-on > SAML.
- In PulseWatch go to Settings > Authentication, add a provider of type SAML 2.0 and save it. The dialog then shows the three values for Entra: Identifier (entity ID), Reply URL (ACS) and the metadata URL. Enter identifier and reply URL in Entra.
- Copy Login URL, Microsoft Entra Identifier and the Base64 signing certificate from Entra into the provider and save. Assertions must be signed; unsigned ones are rejected.
- Optional: map the groups claim (object IDs) to roles like for OIDC.
Login is service-provider initiated only. The short-lived state cookie is SameSite=None; Secure, so the base URL must be https (or localhost); behind a proxy without https the login fails with state_expired.
State lives in two places: the MariaDB database and the secret key (PW_SECRET_KEY, or secret.key in the data volume). Back up both, a database dump without the key cannot be decrypted.
# Backup (while running)
docker compose exec mariadb sh -c 'mariadb-dump -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" \
--single-transaction --routines "$MARIADB_DATABASE"' | gzip > pulsewatch-$(date +%F).sql.gz
docker run --rm -v pulsewatch_data:/data -v "$PWD":/backup alpine tar czf /backup/pulsewatch-data-$(date +%F).tgz -C /data .
# Restore into an empty database
docker compose up -d mariadb
gunzip -c pulsewatch-2026-01-01.sql.gz | docker compose exec -T mariadb sh -c 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"'
docker run --rm -v pulsewatch_data:/data -v "$PWD":/backup alpine tar xzf /backup/pulsewatch-data-2026-01-01.tgz -C /data
docker compose up -d(The volume prefix pulsewatch_ is the compose project name; check docker volume ls.)
The heartbeat table is the largest part. If dumps get big, rely on rollups: lower PW_RETENTION_RAW_DAYS. For a quick, key-independent copy of your configuration use Settings > Backup (YAML/JSON export, secrets masked by default), see Config as code.
git pull # or fetch the new release
docker compose build --pull
docker compose up -dMigrations run automatically at startup under a database lock, so several replicas can start at once. They are forward-only: take a database backup first. Check the release notes for new environment variables before upgrading; unknown variables are ignored.
Kuma 2.x has no backup export, so two other sources are supported (Settings > Backup > Import from Uptime Kuma 1.x / 2.x):
- Database file: upload
data/kuma.db(SQLite, Kuma 1.x or 2.x). It is streamed toPW_DATA_DIR/import-tmp, opened read-only and deleted right after; only the configuration tables are read, so a multi-gigabyte heartbeat history is fine. Copy the file while Kuma is stopped (or together with its-walfile). - Live API: enter the URL, username and password of a running Kuma (and the two-factor code if the account has one). PulseWatch logs in over Kuma's Socket.IO API once, reads the lists Kuma pushes and logs out; the login is not stored. A Kuma that runs on MariaDB can only be imported this way.
Both show a preview (what would be created or updated, plus warnings) and write nothing until you press apply; the preview is kept in memory for 15 minutes. The import runs as a background job with a progress bar, so a large Kuma 2.x no longer blocks the page. The URL may carry a trailing slash or a page such as /dashboard; PulseWatch cuts it down to the base address.
What gets imported: groups, tags (with values), notification channels (email, Telegram, Discord, Slack, Teams, ntfy, Gotify, Pushover, webhook), monitors (including the protocols PulseWatch has runners for: NTP, RADIUS, RabbitMQ, Kafka, Steam, GameDig, manual, PM2, Oracle, Globalping, SFTP, SMTP, SNMP, ...; Docker hosts become the monitor's endpoint), maintenance windows (single, daily, weekly, monthly and cron ones, targets are the linked monitors) and status pages (sections, monitors, custom domain, title, description, the pinned incident as an announcement; the Kuma entry page becomes the home page). Kuma's server time zone becomes the default for maintenance windows, and its certificate-expiry days become the warning threshold of the imported monitors. In the preview each section has a master checkbox and per-item checkboxes, so you can import only the status pages or only some monitors, and rows where something changes on the way (for example "upside down mode is not supported") carry a note.
What cannot be imported is listed in its own section of the preview, with a reason per row, a copy button and a CSV download: Tailscale ping and unknown monitor types, notification types without an equivalent, manual and rarely repeating maintenance windows, settings that are environment variables here (data retention, trust proxy, base URL, ...), proxies, API keys and remote browsers. Secrets such as Kuma's JWT secret are never read. Existing items with the same name or slug are updated, nothing else is touched, so running it twice is safe.
For a Kuma 1.x backup JSON use the second card on the same page: The importer converts monitors (including nested groups), tags and notification channels, reports what it skipped (unsupported monitor types are listed as warnings) and merges by name. Credentials contained in the backup are re-encrypted with your key.
Measured with test/load/scheduler.ts on a 2 vCPU / 8 GB virtual machine, MariaDB 11 on the same machine, TCP monitors against a local listener, 75 s runs:
| Monitors | Interval | Checks/s | Achieved vs. ideal | Peak scheduler lag | Peak RSS | App CPU (share of one core) |
|---|---|---|---|---|---|---|
| 2,000 | 30 s | 66 | 99 % | 1.3 s | 199 MB | 9 % (7.1 s CPU in 75 s) |
| 5,000 | 30 s | 165 | 99 % | 1.1 s | 264 MB | 15 % (11.5 s CPU in 75 s) |
"Achieved vs. ideal" is the number of checks that ran divided by the number the intervals call for (the run is cut off after 75 s, so a few checks of the last interval are not counted). CPU is the PulseWatch process only, MariaDB comes on top. These numbers measure the scheduler, database writes and rollup bookkeeping. Real checks are slower than a loopback TCP connect (HTTP with TLS, DNS) and hold a concurrency slot while they wait, so the practical ceiling depends on your targets' latency: with PW_MAX_CONCURRENCY=200 and an average check time of 500 ms you can sustain about 400 checks per second. Raise the limit or add PW_ROLE=worker containers beyond that. Not tested: multi-day soak runs, more than 5,000 monitors, MariaDB on slow disks.
- Anomaly hint: flags response-time drift against the monitor's own baseline, without any AI service.
- "What changed?" (Insights): correlates an incident with other monitors that went down at the same time, in the same group, behind the same dependency or on the same host.
- Uptime heatmap calendar (Insights): one square per day, up to a year.
- Uptime badges: SVG badges (
/badge/<id>/status.svg,uptime-30d.svg,ping.svg) for READMEs, opt-in per monitor. - Daily digest: one summary mail per day (Settings > Daily digest, with preview).
- Language of notifications: alerts (down, up, flapping, certificate, test), the daily digest, the SLA report mail and its PDF/CSV attachment are written in English or German. Choose the instance-wide language on Notifications (Language of notifications and emails; or fix one language for everything under Settings > Languages); without a saved choice the language of the first administrator profile that has one is used, otherwise English. People who pick a language in their own profile get emails, report attachments and SLA downloads in that language (Automatic follows the instance); a report goes out as one mail per language.
GET /api/reports/slaacceptslang=en|deto override it. German CSV files start with a byte order mark so Excel shows the umlauts. In German the subject tags read[OFFLINE],[ONLINE],[INSTABIL],[ZERTIFIKAT]instead of[DOWN],[UP],[FLAPPING],[CERTIFICATE]. Templates you wrote yourself are sent as written, and the error text of a check (for example a timeout message) stays English. - Kiosk mode:
/kioskis a wall display. A colored header band states the overall situation (with counts, 24-hour availability, average response time and a clock), active incidents run in a strip below it, and the tiles show group, how long a state has lasted, heartbeat bar, response time and, for game servers, the players. Problems come first, and a new outage jumps to page 1. PressS(or the gear) for settings: which monitors to show (groups and tags that follow changes, or single monitors picked by hand, with one-click group chips), All / Problems only / Everything but only problems while there are any, the layout (All tiles together or Grouped, one heading per group with its path, how many of its monitors are up and a colored dot; a group that continues on the next page repeats its heading), tile size, a stepless zoom of 50 to 200 percent for the whole page (also+,-and0for 100 percent), page switch interval, dark or light look, the history bars, incident strip and clock. The settings live in the address (/kiosk?groups=2&layout=grouped&size=l&zoom=120&rotate=20&theme=light,monitors=1,2,3for hand-picked ones), so a copied link sets up another screen; older?group=links keep working.Spacepauses,←→change pages,Fis full screen,Escleaves. The screen is kept awake while it is open and the mouse pointer hides itself. - Command palette and shortcuts:
Ctrl/Cmd+Kjumps to any monitor or page. - Dead-man's switch:
PW_DEADMAN_URLmakes PulseWatch ping an external service so you notice when PulseWatch itself is down. - Saved views and flap detection on the dashboard and per monitor.
- Bulk-add preview: pasted spreadsheet columns are parsed live, duplicates are flagged before anything is created, failed lines stay in the box.
- Open-incident count in the tab title, so a background tab shows
(2) Dashboard. - Whole-group import for status pages, including subgroups.
- SAML is verified with locally signed assertions (valid and tampered, wrong audience, replay, unsolicited), not against a real Entra ID tenant. IdP-initiated login, metadata-URL import and group overage (more than 150 groups) are not supported.
- Custom CAs are tested with HTTP and TLS checks against a local server; the database, gRPC and MongoDB drivers only compile with the option. The CA list is re-parsed per connection (
shortcut:insrc/core/trust.ts). - OIDC, LDAP and passkeys are covered by code review and unit-level checks only; they have not been tested against live Keycloak/Entra ID/OpenLDAP servers or real authenticators. Try them on a staging instance first.
- Probe-only monitors are evaluated on the monitor's schedule, not at the instant a probe reports.
- Tracing exports one flat span per check (no parent/child context propagation).
- The Kuma 2.x import (database file and API) is covered by tests with a generated SQLite file and a fake Socket.IO server that follows Kuma's protocol as documented, not by a real Kuma 2.x instance. Kuma column names and units for the newer monitor types, maintenance strategies and settings are written from the documented schema, not checked against a real instance. Check history, proxies, API keys, remote browsers and Tailscale ping monitors are not imported.
- The newer monitor types are tested against fake servers that follow the protocols as documented, not against the real products: NTP, RADIUS, FTP, RDP, SIP, LDAP, Kafka, RabbitMQ, Steam/A2S, GameDig (through a Source query), Pelican, AMP and Globalping. Pelican and AMP use their documented HTTP APIs (AMP instance states are taken from its public enum). Oracle is only tested against a closed port and has no TLS (TCPS) support. Kafka is a broker check (metadata and topic leaders, no SASL, no message is produced). PM2 needs the
pm2command on the machine that runs the check, which the container image does not have: use a probe on the PM2 host. There is no real-browser monitor. - Two status-page widgets are simple by design: components cannot be reordered across sections by drag and drop, and the kiosk page skips paused monitors.
- Only MariaDB 11 is supported (MySQL may work but is not tested).






