Repository navigation
Configuration
Language: English · فارسی
Every key with its type and default is in the YAML reference. Default file name: pulsecheck.yml (change with -c). Any ${NAME} inside a value is replaced with the environment variable NAME. A missing variable is an error, so keep secrets out of the file.
Inside inline
{ ... }mappings, quote values that contain${VAR}:{name: api, url: "https://x.example.com/${TOKEN}"}.
state_file: pulsecheck.db
shoutrrr_bin: shoutrrr
notify:
urls: ["telegram://${TG_TOKEN}@telegram?chats=${TG_CHAT}"]
events: [down, up, degraded]
defaults: {timeout: 10, down_after: 2, interval: 5, slow_ms: 3000}
maintenance:
- {name: weekly, start: "SUN 02:00", duration: 60, sites: [api]}
sites:
- {name: web, url: https://example.com}| Key | Default | Meaning |
|---|---|---|
state_file |
pulsecheck.db |
SQLite file holding current state and check history (kept 400 days) |
shoutrrr_bin |
auto |
auto (docker image if Docker works, else the shoutrrr binary), docker, or a path to a binary |
shoutrrr_image |
nickfedor/shoutrrr:0.21.2 |
Docker image used in auto/docker mode |
notify |
none | Global alert targets, see below |
defaults |
{} |
Any site key; merged into every site (a site's own value wins) |
messages |
built-in | Alert texts by key, see Messages |
otel |
none | Push results as OpenTelemetry, see OpenTelemetry |
maintenance |
[] |
Windows where alerts are suppressed |
sites |
required | List of checks; name must be unique |
| Key | Default | Applies to | Meaning |
|---|---|---|---|
name |
required | all | Unique name, used in alerts and reports |
type |
http |
all |
http, tcp or ping
|
url |
– | http | Full URL (required) |
host |
– | tcp, ping | Host name or IP (required) |
port |
– | tcp | Port (required) |
method |
GET |
http | HTTP method |
headers |
{} |
http | Request headers |
body |
– | http | Request body (string) |
expected_status |
any 2xx | http | List such as [200, 301]
|
max_redirects |
5 |
http | Redirects are followed up to this number |
max_response_ms |
off | all | Slower than this counts as a failure |
slow_ms |
off | all | Slower than this (but successful) is degraded |
timeout |
10 |
all | Seconds |
down_after |
2 |
all | Consecutive failures before the site is marked down |
interval |
5 |
all | Minutes between checks (see below) |
tags |
[] |
all | Free labels, e.g. [prod, db]. Used by --tag, and as {tags} in messages |
ssl_warn_days |
off | http | Warn when the https certificate expires in fewer days; an expired certificate is a failure. Not available on Cloudflare |
remind_every |
0 (off) |
all | Minutes between repeated "still down" alerts while a site stays down (also paces repeated certificate warnings) |
notify |
global | all | Overrides the global notify for this site |
messages |
global | all | Overrides message texts for this site only |
- A successful check is
up, ordegradedif it took at leastslow_ms. - A failed check only marks the site
downafterdown_afterfailures in a row. Shorter blips never alert. - Alerts are sent only when the status changes:
down,degraded, orup(recovery; the message includes how many minutes the site was affected). -
eventsfilters which ofdown,up,degraded,ssl_warnare sent (a repeated "still down" reminder counts asdown). A site-levelnotifyreplaces the global one completely, includingevents. - Alert failures (missing binary, bad URL) are printed to stderr and never stop the run.
-
remind_every: 60sends theremindermessage every 60 minutes while the site stays down.0(default) sends the firstdownalert only. -
ssl_warn_days: 14checks the https certificate on every check. When fewer than 14 days remain you get anssl_warnalert, at most once a day (or everyremind_everyminutes if set). If the certificate has expired the check fails withTLS certificate has expired, which is a normaldown. - Unknown keys anywhere in the file are errors, so typos are caught by
validate.
By default the operating system picks the address family. To check one family on purpose, set ip_version on the site:
sites:
- {name: web-v4, url: "https://example.com", ip_version: 4}
- {name: web-v6, url: "https://example.com", ip_version: 6} # down if the host has no AAAA record or IPv6 is unreachable
- {name: dns-v6, type: tcp, host: "2001:4860:4860::8888", port: 53} # an IPv6 literal needs no ip_version
- {name: gateway-v6, type: ping, host: gw.example.com, ip_version: 6} # runs `ping -6`It applies to http, tcp, ping and the certificate check (ssl_warn_days). Giving the same host two entries, one per family, tells you which side broke. If the machine running pulsecheck has no IPv6 route, an ip_version: 6 site is reported down; the error says why (Network is unreachable, Address family for hostname not supported, or Name or service not known). Cloudflare Workers cannot choose a family, so those sites are skipped there.
Give sites tags: [prod] and limit commands with --tag prod (run, status, report).
pulsecheck status prints the current state of every site (--json for scripts, --md for a markdown table, for example to append to $GITHUB_STEP_SUMMARY). There is no dashboard; this is the status view. On Vercel it is GET /api/status, on Cloudflare GET /status (both need the token, see Serverless).
Your scheduler decides how often pulsecheck runs; interval only skips sites whose last check is too recent. Example: run every 5 minutes, give a heavy check interval: 60 and it runs hourly. A one-minute margin absorbs scheduler jitter, so interval: 5 with a 5-minute schedule checks every run.
Times are UTC. start is "HH:MM" (every day) or "DAY HH:MM" with MON TUE WED THU FRI SAT SUN. duration is in minutes and may cross midnight. Empty sites means all sites. Checks still run and are recorded, but alerts raised during the window are dropped, not delivered later.
Every alert text has a key. The code uses only these keys, and you can change any of them under messages: (globally, or inside a single site). Keys you don't set keep the built-in text.
| Key | Sent when | Built-in text |
|---|---|---|
down |
site went down | 🔴 {site} is DOWN: {error} |
degraded |
site is up but slow | 🟡 {site} is slow ({ms}ms) |
up |
site recovered | 🟢 {site} is UP ({ms}ms) after {minutes}m |
reminder |
site still down after remind_every
|
🔴 {site} is still DOWN after {minutes}m: {error} |
ssl_warn |
certificate expires soon | 🔒 {site}: TLS certificate expires in {days} days |
test |
pulsecheck test-notify |
pulsecheck test notification |
Placeholders: {site} name, {event} (down/degraded/up), {error} failure reason (empty on success), {ms} response time, {minutes} minutes since the last status change, {status} HTTP status code (empty for tcp/ping), {tags} the site's tags joined by commas, {days} days until certificate expiry (only in ssl_warn). Use {{ and }} for literal braces. Unknown keys or placeholders are rejected by validate. Order of precedence: site messages > global messages > built-in.
messages:
down: "ALERT {site} unreachable ({error})"
up: "{site} recovered after {minutes} min"
sites:
- name: payments
url: https://pay.example.com
messages: {down: "💳 payments DOWN, page on-call"}Set otel: to push every run to an OpenTelemetry collector (or any service that accepts OTLP/HTTP JSON, which makes it work like a webhook). No OpenTelemetry package is needed, and it works on every platform.
otel:
endpoint: https://otel-collector.example.com:4318 # /v1/metrics and /v1/logs are appended
headers: {Authorization: "Bearer ${OTEL_TOKEN}"}
resource: {env: prod} # extra resource attributes; service.name defaults to "pulsecheck"
# service_name: pulsecheck
# metrics: true # per check: pulsecheck.up (1/0), .response_time (ms), .http.status_code, .ssl.days_remaining
# logs: true # one log record per alert (ERROR for down, WARN for slow/ssl, INFO for recovery)Metric attributes: site, type, state (up/down/degraded) and tags. A failed export is printed to stderr and never fails the run. pulsecheck test-notify sends a test log record to the endpoint.
pulsecheck report shows, for day / week / month / year: uptime %, average and p95 response time of successful checks, and the number of checks. --json prints the same data for scripts. On Vercel/Cloudflare the state store keeps daily totals only, so report there has no p95 and covers the last 90 days.