Check Types
SolidPing supports 40 check types across multiple categories for monitoring your services. Each check type has specific configuration options and validation capabilities.
Network Checks
HTTP/HTTPS
Monitor web services, APIs, and websites.
URL Format:
http://example.com/path
https://api.example.com/health
| Option | Description | Example |
|---|---|---|
| URL | The endpoint to check | https://api.example.com/health |
| Method | HTTP method | GET, POST, PUT, DELETE, QUERY |
| Timeout | Request timeout | 30s |
| Expected Status | Status code to expect | 200, 2XX (wildcard) |
| Headers | Custom request headers | Authorization: Bearer token |
| Body | Request body (for POST/PUT/PATCH/QUERY) | {"key": "value"} |
| Response assertions | Assert on the response body — see Response assertions below | bodyAssertions, json_path_assertions, body_expect |
| SSH tunnel | Dial through an SSH check's bastion | An ssh check with expected_fingerprint set |
| Basic Auth | Username and password — stored encrypted at rest | user:password |
| Custom User-Agent | Override the default user-agent | SolidPing/1.0 |
Status Code Matching:
- Exact match:
200,201,404 - Wildcard:
2XX(any 2xx status),5XX(any 5xx status)
QUERY method: QUERY is sent with the request body attached, exactly like
POST, and is treated like POST for redirects (a 307/308 re-sends the
method and body; 301/302/303 behave the same as they do for any other
non-GET/HEAD method).
Basic Auth storage: you still enter a username and a password in the form,
but the pair is stored as a single encrypted credential (a reserved basicAuth
config key) — both halves are protected, not just the password. The dashboard
never gets the credential back: an existing one renders as
•••• (encrypted — enter new values to replace), and editing anything else on
the check leaves it (and any secret headers) untouched. To change it, retype
both fields; to remove it, clear both and save. Checks written before this
change keep working and fold into the new shape the next time they are saved.
Examples:
# Basic health check
url: https://api.example.com/health
method: GET
expected_status: 200
timeout: 10s
# API with authentication
url: https://api.example.com/v1/users
method: GET
headers:
Authorization: Bearer your-token
Accept: application/json
expected_status: 200
body_expect: '"users":'
# POST request
url: https://api.example.com/webhook
method: POST
headers:
Content-Type: application/json
body: '{"test": true}'
expected_status: 200
# ASP.NET Core health endpoint — a bare word as text/plain
url: https://api.acme.com/health
method: GET
expected_status_codes: ["2XX"]
bodyAssertions:
type: assertion
operator: eq
value: Healthy
ignoreCase: true
Response assertions
A check that only looks at the status code reports a degraded service as up:
plenty of health endpoints answer 200 while telling you, in the body, that
they are unwell. Three assertion families cover that, and they can be combined
on one check — all of them must pass for the check to be up.
Body assertions (bodyAssertions)
Treats the response body as plain text. This is the one to reach for on a
text/plain endpoint, and the only one that does exact equality.
url: https://api.acme.com/DictionaryApi/health
expected_status_codes: ["2XX"]
bodyAssertions:
type: assertion
operator: eq
value: Healthy
ignoreCase: true
| Field | Description |
|---|---|
type | assertion for a single test, or and / or for a group with children |
operator | eq, neq, contains, not_contains, regex |
value | What to compare the body against |
ignoreCase | true compares without regard to case (and folds the regex) |
Whitespace: eq and neq compare against the body with leading and
trailing whitespace removed, so an endpoint returning "Healthy " still
matches eq: Healthy. contains, not_contains and regex see the body
exactly as it arrived.
exists, not_exists and the numeric comparisons are rejected here: a raw
body always exists, so accepting them would build an assertion that can never
fail.
Groups nest, so "healthy or degraded, but never containing a stack trace" is:
bodyAssertions:
type: and
children:
- type: or
children:
- { type: assertion, operator: eq, value: Healthy, ignoreCase: true }
- { type: assertion, operator: eq, value: Degraded, ignoreCase: true }
- { type: assertion, operator: not_contains, value: "at System." }
JSONPath assertions (json_path_assertions)
Parses the body as JSON and tests one value plucked out of it with a
JSONPath expression. Same node
shape, plus a path, and with the full operator list: eq, neq, gt,
gte, lt, lte, contains, not_contains, regex, exists,
not_exists. ignoreCase applies to the textual ones.
url: https://status.acme.com/api/v2/status.json
expected_status_codes: ["2XX"]
json_path_assertions:
type: and
children:
- { type: assertion, path: "$.status.indicator", operator: eq, value: none }
- { type: assertion, path: "$.uptime", operator: gt, value: "0" }
A body that is not valid JSON fails the check rather than being skipped.
The flat matchers
Older, simpler and still supported. They are what the importers map foreign conditions onto, so they are not going anywhere.
| Key | Meaning |
|---|---|
body_expect | Substring that must appear in the body |
body_reject | Substring that must not appear |
body_pattern | RE2 regex the body must match |
body_pattern_reject | RE2 regex the body must not match |
headers_pattern | Map of response header name to an RE2 regex; every one must match |
body_expect is a substring match, which is why bodyAssertions exists:
body_expect: HEALTHY also matches a body of UNHEALTHY, and reports a dead
service as up.
TCP
Monitor TCP services like databases, message queues, and custom services.
URL Format:
tcp://hostname:port
tcps://hostname:port # With TLS
| Option | Description | Example |
|---|---|---|
| Host | Target hostname | db.example.com |
| Port | Target port | 5432 |
| TLS | Enable TLS/SSL | true / false |
| Timeout | Time budget for the whole exchange: connect, TLS, send and wait for the reply | 10s |
send_data | Payload to send once connected (after the TLS handshake for tcps) | PING\r\n |
send_encoding | How send_data becomes bytes: text (default), escaped, hex | escaped |
expect_data | Substring the reply must contain | +PONG |
expect_encoding | How expect_data becomes bytes: text (default), escaped, hex | hex |
expect_pattern | RE2 regex the reply must match | ^220 .* ESMTP |
| SSH tunnel | Dial through an SSH check's bastion — the hostname is resolved by the bastion | An ssh check with expected_fingerprint set |
UDP
Check a UDP service by sending it something and asserting the answer.
URL Format:
udp://hostname:port
| Option | Description | Example |
|---|---|---|
| Host | Target hostname | dns.example.com |
| Port | Target port | 53 |
| Timeout | Time budget for the whole exchange: connect, send and wait for the reply | 10s |
send_data | Datagram to send | 5350 0100 0001 ... |
send_encoding | How send_data becomes bytes: text (default), escaped, hex | hex |
expect_data | Substring the reply must contain | 53508180 |
expect_encoding | How expect_data becomes bytes: text (default), escaped, hex | hex |
expect_pattern | RE2 regex the reply must match | ^[\x1c\x24] |
UDP has no handshake. A datagram sent into the void succeeds whether or not
anything is listening, so a UDP check without expect_data or
expect_pattern can only ever fail on an ICMP port-unreachable. Always set an
expectation.
Send a payload and wait for a reply
Completing a TCP handshake proves a firewall forwards the port, not that the
service behind it works. tcp and udp checks can send a payload and require
an answer — which is what actually proves the service is alive.
Redis over TCP — send PING, require +PONG:
host: redis.example.com
port: 6379
send_data: 'PING\r\n'
send_encoding: escaped
expect_pattern: '^\+PONG'
DNS over UDP — send a real query for example.com A and require an answer
carrying the same transaction ID, with RCODE 0:
host: 8.8.8.8
port: 53
send_encoding: hex
send_data: "5350 0100 0001 0000 0000 0000 07 6578616d706c65 03 636f6d 00 0001 0001"
expect_encoding: hex
expect_data: "53508180"
How it behaves:
- Encodings.
textsends the string byte-for-byte (the default, so nothing stored before encodings existed changes meaning).escapeddecodes the C-style escapes\r,\n,\t,\0,\\and\xNN— the only way to express a CRLF in a form field.hexdecodes hex digits, whitespace ignored. - Both expectations apply.
expect_dataandexpect_patternmay both be set; the reply must satisfy both. - The reply is read until it matches, not once. A banner split across two
segments, or an answer that arrives after a greeting, matches — up to a 4 KB
cap on the reply. Matching runs on the whole buffer; the
received_dataoutput field is capped at 1 KB and rendered with\xNNescapes when the reply is not valid UTF-8. - Silence is a
Timeout, not aDown: a port that accepts a connection and then says nothing is a distinct failure, reported asno matching reply within 10s (0 bytes received). - A payload with no expectation keeps its old meaning: the reply is read once for diagnostics and silence is not a failure.
- A raw byte above
0x7Fis not valid UTF-8 and cannot be written as\xNNinexpect_pattern— assert it with a hexexpect_datainstead.
ICMP (Ping)
Check host availability using ICMP echo requests.
URL Format:
ping://hostname
icmp://hostname
| Option | Description | Default |
|---|---|---|
| Host | Target hostname or IP | - |
| Count | Number of packets | 3 |
| Interval | Time between packets | 1s |
| Timeout | Total timeout | 10s |
ICMP checks may require elevated permissions on some systems. Docker containers typically need NET_RAW capability.
DNS
Verify DNS resolution and record values.
URL Format:
dns://resolver/domain?type=A
dns://8.8.8.8/example.com?type=MX
| Option | Description | Example |
|---|---|---|
| Resolver | DNS server to query | 8.8.8.8, 1.1.1.1 |
| Domain | Domain to resolve | example.com |
| Type | Record type | A, AAAA, MX, TXT, CNAME, NS, SOA |
| Expected | Expected values | 93.184.216.34 |
WebSocket
Monitor WebSocket endpoint availability.
URL Format:
ws://hostname/path
wss://hostname/path # With TLS
| Option | Description | Example |
|---|---|---|
| URL | WebSocket endpoint | wss://api.example.com/ws |
| Timeout | Connection timeout | 10s |
RDP (Remote Desktop)
Monitor Remote Desktop Protocol servers. Unlike a plain TCP/3389 port probe, this checker performs the pre-auth RDP negotiation handshake (X.224 Connection Request/Confirm, MS-RDPBCGR): a valid answer proves the RDP listener (TermService, xrdp, …) actually parsed the request — not just that a firewall forwards the port. The handshake needs no credentials and stops before any authentication.
With username and password set, the check goes further: it completes a real interactive Windows logon — CredSSP/NLA, the full connection sequence, a settle wait until the screen stops painting, an optional screenshot, and an explicit session end (log off by default, or disconnect). See the caveats below; this mode is a user-visible logon, not a probe.
| Option | Description | Default |
|---|---|---|
| Host | RDP server hostname or IP | - (required) |
| Port | TCP port | 3389 |
| Timeout | Check timeout (max 30s pre-auth, 90s with credentials) | 5s pre-auth, 45s with credentials |
| Require NLA | Mark down unless the server selects Network Level Authentication (CredSSP) — catches NLA silently disabled by policy | off |
| Cert warning (days) | Mark warning when the server certificate expires in at most this many days. 0 = off | off |
| Cert critical (days) | Mark down when the server certificate expires in at most this many days. Must be ≤ Cert warning. 0 = off | off |
| Username / Password | Perform an authenticated interactive logon. Both must be set together | off |
| Domain | Windows domain for the logon (optional; local accounts leave it empty) | off |
| Screenshot | Capture a PNG of the desktop once the logon settles (authenticated runs only) | off |
| End session | logoff (default) or disconnect | logoff |
- Default verdict = TCP connect + a valid X.224 Connection Confirm. A negotiation failure (
RDP_NEG_FAILURE, e.g.HYBRID_REQUIRED_BY_SERVER) or a non-RDP answer yields down. - The negotiated security protocol (
rdp,tls,nla,nla_ex,rdstls) and the server's negotiation flags are reported in the check output. - When a TLS-based protocol is selected, the checker completes one TLS handshake to read the server certificate (subject, issuer, expiry, self-signed flag). RDP certificates are routinely self-signed or from an internal CA, so only the leaf's expiry is inspected — the chain is deliberately not validated. An already-expired certificate is always down.
- Authenticated failure states carry distinct codes in the check output:
auth_rejected(bad password, locked account, or a domain that disabled NTLM),logon_timed_out(the desktop never settled within the timeout), andsession_disconnected(the server ended the session — licensing, policy, or "another user is logged on"). - A failed log off or screenshot after a successful logon is reported in the output details but never turns an
uprun down: the logon itself succeeded. - The check runs at most 4 RDP logons at a time per worker; a saturated worker reports a timeout on the late check rather than queueing invisibly.
Every authenticated run is a real interactive Windows logon:
- it loads a user profile and runs logon scripts / GPOs;
- it may consume an RDS client access license;
- on a single-session server (or with "restrict to one session per user") it can disconnect a real logged-in user;
- it shows up in the Security event log (4624/4634) every run.
So: use a dedicated monitoring account, never a person's account, and keep the interval long — the minimum period is 15 minutes for every authenticated run, enforced by the API. Authentication is NTLM through CredSSP only; Kerberos is not supported, so domains that disable NTLM will fail at NLA with a clear error.
RDP hosts are typically reachable only from inside a network — run the check from a worker with network access to the host. The handshake is pre-auth and closes cleanly, so it generates connection events but no authentication-failure noise in Windows event logs.
Security & Certificates
SSL/TLS Certificate
Monitor SSL/TLS certificate expiration and validity.
| Option | Description | Example |
|---|---|---|
| Host | Target hostname | example.com |
| Port | HTTPS port | 443 |
| Warning Days | Days before expiry to warn | 30 |
| Critical Days | Days before expiry to alert | 7 |
What gets checked:
- Certificate validity
- Expiration date
- Chain validation
- Hostname verification
Domain Expiration
Monitor domain name registration expiration. Lookups go through RDAP (RFC 7480–7484), the structured, HTTPS-based successor to WHOIS, with an automatic fallback to WHOIS when RDAP can't answer (no RDAP service for the TLD, a request error, or a response with no expiration date).
| Option | Description | Example |
|---|---|---|
| Domain | Domain name to check | example.com |
| Warning Days | Days before expiry to warn | 30 |
| Critical Days | Days before expiry to alert | 7 |
| Lookup method (Advanced) | auto (default, RDAP first with WHOIS fallback), rdap (RDAP only, no fallback), or whois (WHOIS only) | auto |
The check result reports which path answered via a method field
("rdap" or "whois") alongside the existing expiry_date, days_remaining,
and registrar fields.
Expiry is graduated across two tiers, exactly like the SSL/TLS check: at or below Critical Days the check goes down (pages); between Critical and Warning Days it reports warning (amber, counts as up, no incident). Existing checks that only set the legacy single threshold keep working unchanged — it's treated as Critical Days, with Warning Days defaulting to the same value.
DNSBL (DNS Blocklist)
Check whether an IP address or hostname is listed on DNS-based blocklists (DNSBLs) such as Spamhaus, SpamCop, Barracuda, or UCEPROTECT. This is essential for monitoring the reputation of mail servers and public IPs.
| Option | Description | Default |
|---|---|---|
| Target | IPv4 address or hostname to check | - (required) |
| Blocklists | List of DNSBL zones to query | zen.spamhaus.org, bl.spamcop.net, b.barracudacentral.org, dnsbl-1.uceprotect.net |
| Nameserver | Custom DNS resolver (host:port) | system resolver |
| Timeout | Query timeout (max 60s) | 10s |
Hostnames are resolved to IPv4 before lookup. The check fails when the target is listed on at least one blocklist, and succeeds when it is clean. If every queried zone errors out, the result is inconclusive (timeout).
Error/status codes. DNSBLs reserve the 127.255.255.0/24 range for error and status replies rather than real listings — Spamhaus, for example, returns 127.255.255.254 when a query arrives via a public/open resolver (refused) and 127.255.255.255 when a rate limit is exceeded. SolidPing treats any 127.255.255.x answer as an error code, not a listing: the affected zone is reported as inconclusive and the raw code is surfaced under error_codes in the check output.
Spamhaus from cloud IPs. Because workers run on public cloud (and use the provider's shared resolvers), the public zen.spamhaus.org zone will typically be refused with 127.255.255.254. For reliable Spamhaus results, use the Spamhaus Data Query Service (DQS): configure the DQS account-key blocklist zones and a dedicated resolver via the Blocklists and Nameserver options rather than querying the public zen.spamhaus.org zone.
Database Checks
PostgreSQL
Monitor PostgreSQL database connectivity and query execution.
URL Format:
postgres://user:password@hostname:5432/database
| Option | Description | Example |
|---|---|---|
| URL | Connection string | postgres://user:pass@db:5432/mydb |
| Query | Optional test query | SELECT 1 |
| Timeout | Connection timeout | 10s |
MySQL / MariaDB
Monitor MySQL or MariaDB database connectivity and query execution.
URL Format:
mysql://user:password@hostname:3306/database
| Option | Description | Example |
|---|---|---|
| URL | Connection string | mysql://user:pass@db:3306/mydb |
| Query | Optional test query | SELECT 1 |
| Timeout | Connection timeout | 10s |
MongoDB
Monitor MongoDB connectivity using the ping command.
URL Format:
mongodb://user:password@hostname:27017/database
| Option | Description | Example |
|---|---|---|
| URL | Connection string | mongodb://user:pass@db:27017/mydb |
| Timeout | Connection timeout | 10s |
Redis
Monitor Redis server availability using the PING command.
URL Format:
redis://hostname:6379
redis://:password@hostname:6379
| Option | Description | Example |
|---|---|---|
| URL | Connection string | redis://redis:6379 |
| Timeout | Connection timeout | 10s |
Microsoft SQL Server
Monitor MSSQL database connectivity and query execution.
URL Format:
sqlserver://user:password@hostname:1433?database=mydb
| Option | Description | Example |
|---|---|---|
| URL | Connection string | sqlserver://sa:pass@db:1433?database=mydb |
| Query | Optional test query | SELECT 1 |
| Timeout | Connection timeout | 10s |
Oracle Database
Monitor Oracle database connectivity and query execution.
URL Format:
oracle://user:password@hostname:1521/service
| Option | Description | Example |
|---|---|---|
| URL | Connection string | oracle://user:pass@db:1521/orcl |
| Query | Optional test query | SELECT 1 FROM DUAL |
| Timeout | Connection timeout | 10s |
ClickHouse
Monitor ClickHouse connectivity and query execution over the native (binary) protocol — not the HTTP interface — so the check exercises the same transport your analytics clients use.
| Option | Description | Example |
|---|---|---|
| Host | Server hostname | clickhouse.example.com |
| Port | Native port. Defaults to 9000, or 9440 when TLS is on | 9000 |
| Username | Optional, defaults to ClickHouse's default user | monitor |
| Password | Optional password | |
| Database | Optional, defaults to default | metrics |
| Use TLS | Native protocol over TLS (required by ClickHouse Cloud) | false |
| Verify TLS certificate | Validate the server certificate. Requires TLS | false |
| Query | Optional test query, must start with SELECT | SELECT 1 |
| Timeout | Connection timeout (max 30s) | 10s |
The check pings the server, then runs the query and reports its first cell. The
server version is reported in the result output, and connection_time_ms /
query_time_ms / total_time_ms are recorded as metrics.
Email Services
SMTP
Monitor SMTP server availability with optional authentication.
URL Format:
smtp://hostname:25
smtp://hostname:587
smtps://hostname:465 # With SSL
| Option | Description | Example |
|---|---|---|
| Host | SMTP server | smtp.example.com |
| Port | SMTP port | 25, 587, 465 |
| STARTTLS | Enable STARTTLS | true / false |
| Auth | Test authentication | user:password |
| Timeout | Connection timeout | 10s |
Send mode: a real probe email
By default the SMTP check stops at the handshake (EHLO/STARTTLS/AUTH) — it proves the server is reachable and, optionally, that credentials work, but not that mail submitted to it actually gets delivered. Send mode closes that gap: on every check execution, after the normal handshake, SolidPing submits a real, system-generated email through the monitored server and addresses it to a paired Email Reception check's tokenized address.
| Option | Description |
|---|---|
| Send a probe email | Enables send mode |
| Mail From | Envelope sender for the probe email — the monitored server's outbound policy (SPF/DKIM alignment, relay ACLs) usually dictates it |
| Delivery check | Pick a paired Email Reception check; the dashboard fills in the recipient address from it |
The message itself is entirely system-generated — there is no subject or body field to fill in, by design: this keeps the feature from ever becoming a way to send arbitrary mail through a monitored server. It carries two headers the Email Reception check reads to attribute and time the delivery:
X-SolidPing-Check: <the sending SMTP check's UID>
X-SolidPing-Sent-At: <RFC3339 send time>
If an intermediate mail server strips these headers, the Email Reception check still ticks up as normal — only the attribution and latency are lost.
The two checks split responsibility cleanly:
- The SMTP check's own result reflects submission only — a
250afterDATAis up (withsubmission_msrecorded), any rejection is down with the server's reply. - The paired Email Reception check reflects end-to-end delivery — it goes down (via its normal passive-overdue logic) if probes stop arriving within its period.
Submission failures and delivery failures are different problems with different fixes, so keeping them as two checks gives each its own incident lifecycle.
Sizing the paired check's period. The Email Reception check's period is the delivery deadline. Size it generously:
email check period ≥ SMTP check interval + worst acceptable delivery time
Greylisting in particular can legitimately delay a first-time sender by several minutes — a paired check sized too tightly will flap on nothing but normal greylisting behavior.
A 60-second floor applies to send-mode SMTP checks — every execution sends a real email, so an unbounded fast interval could flood the paired inbox.
Several SMTP checks may target the same Email Reception check — the headers keep each arrival attributable to its sender — but pairing one SMTP check to one dedicated Email Reception check is the recommended setup; a shared target's up/down state otherwise conflates multiple senders. Create the Email Reception check first, then select it from the SMTP check's delivery picker — SolidPing does not offer to create one on your behalf, to keep the two checks' lifecycles independent (delete/rename either one without surprising the other).
Send mode requires the instance to have an email inbox configured (the same one Email Reception checks use to receive mail) — see Email Reception. The recipient address must also be at that inbox's domain; SolidPing rejects anything else.
Under the hood, the recipient address is stored on the check, not resolved at send time. Picking a delivery check in the dashboard just fills in the address for you — the same probe works identically from any worker, including a private location / deported agent, with no extra setup. One consequence worth knowing: SolidPing does not verify that the delivery check you name belongs to your organization once it's just an address — anyone who already knows another check's tokenized address could in principle aim a probe at it too. The receiving inbox domain restriction still holds absolutely (a recipient outside the instance's own inbox is never possible), and the tokenized address itself is exactly as unguessable as it always was.
IMAP
Monitor IMAP server availability and authentication.
URL Format:
imap://hostname:143
imaps://hostname:993 # With SSL
| Option | Description | Example |
|---|---|---|
| Host | IMAP server | imap.example.com |
| Port | IMAP port | 143, 993 |
| TLS | Use implicit TLS | true / false |
| STARTTLS | Enable STARTTLS | true / false |
| Timeout | Connection timeout | 10s |
When creating the check as a JSON config rather than a URL, set "tls": true
explicitly alongside "port": 993 — SolidPing also derives implicit TLS
automatically from port 993 if tls/starttls are both left unset, but
spelling it out keeps a copy-pasted config unambiguous.
POP3
Monitor POP3 server availability and authentication.
URL Format:
pop3://hostname:110
pop3s://hostname:995 # With SSL
| Option | Description | Example |
|---|---|---|
| Host | POP3 server | pop3.example.com |
| Port | POP3 port | 110, 995 |
| TLS | Use implicit TLS | true / false |
| STARTTLS | Enable STARTTLS | true / false |
| Timeout | Connection timeout | 10s |
When creating the check as a JSON config rather than a URL, set "tls": true
explicitly alongside "port": 995 — SolidPing also derives implicit TLS
automatically from port 995 if tls/starttls are both left unset, but
spelling it out keeps a copy-pasted config unambiguous.
Email Reception (Passive Inbox)
Verify end-to-end email delivery rather than just server connectivity. SolidPing generates a unique address for the check; when a message arrives at that address, the check is marked up. This is ideal for monitoring a full sending pipeline (queue → relay → inbox).
| Option | Description | Default |
|---|---|---|
| Token | Secret part of the unique receiving address | auto-generated |
The receiving domain is configured by your administrator. Point a periodic test email (or your application's "send a heartbeat" job) at the generated address, and SolidPing reports an incident if expected mail stops arriving.
Email-reception checks are receive-only — SolidPing waits for mail instead of actively probing a server. Combine it with an SMTP check to also monitor outbound connectivity, or use SMTP send mode to have SolidPing generate and submit that probe email for you automatically.
Two kinds of result rows
Exactly like the Heartbeat check, an email-reception
check writes two kinds of row: the signal row recorded when a message
actually arrives, and the scheduler evaluation row a checks worker writes
every period. Evaluation rows carry evaluation: true, the region of the
worker that wrote them, and lastSignalAt / lastSignalResultUid pointing at
the last email received; signal rows carry none of that. The messages read
Email on time, Email overdue, Last email reported failure and
No email received, following the same table. Branch on evaluation, not on
the message text.
Remote Access
SSH
Monitor SSH server availability.
URL Format:
ssh://hostname:22
| Option | Description | Example |
|---|---|---|
| Host | SSH server | server.example.com |
| Port | SSH port | 22 |
| Timeout | Connection timeout | 10s |
FTP
Monitor FTP server availability.
URL Format:
ftp://hostname:21
| Option | Description | Example |
|---|---|---|
| Host | FTP server | ftp.example.com |
| Port | FTP port | 21 |
| Timeout | Connection timeout | 10s |
SFTP
Monitor SFTP server availability.
URL Format:
sftp://hostname:22
| Option | Description | Example |
|---|---|---|
| Host | SFTP server | sftp.example.com |
| Port | SFTP port | 22 |
| Timeout | Connection timeout | 10s |
| Host key fingerprint | Optional pin on the server's host key | SHA256:uNiVztks… |
Pinning the host key. Leave Host key fingerprint empty and the check
accepts whatever key the server presents — it is a reachability probe against a
host you own. Either way, the fingerprint the server actually presented is
written to the check's output as host_key_fingerprint, so you can read it off
a passing check and paste it back into the field. Once set, a server presenting
a different key fails the check with both fingerprints in the message, and the
rejection happens during the handshake rather than after it. The value uses the
same SHA256:… form as the SSH check, so ssh-keyscan host | ssh-keygen -lf -
produces it.
Messaging & Streaming
gRPC
Monitor gRPC services using the standard health check protocol (grpc.health.v1).
URL Format:
grpc://hostname:50051
| Option | Description | Example |
|---|---|---|
| Host | gRPC server | api.example.com |
| Port | gRPC port | 50051 |
| Service | Service name to check — leave empty for overall server health | my.service.v1 |
| TLS | Encrypt the connection | true / false |
| Skip TLS verification | Accept an invalid, expired or self-signed certificate | true / false |
| Metadata | Request metadata sent on every health RPC, stored in plain text | x-tenant: acme |
| Secret metadata | Same, but stored encrypted — for bearers and API keys | authorization: Bearer … |
| Timeout | Overall check timeout (1–30s) | 10s |
Named service vs overall health. With no service name the check asks for the
server's overall health, which every health server answers. With a service name
it asks about that one service — and if the server never registered it, the
check reports service "…" is not registered with the health server rather than
a raw NotFound RPC error. A service that answers NOT_SERVING is reported
down but still timed, so you can watch a service slow down before it drains.
TLS modes. tls: false speaks plaintext HTTP/2 (h2c). tls: true verifies
the server certificate normally. tls: true with tlsSkipVerify: true encrypts
the connection but accepts any certificate — useful for an internal service with
a self-signed certificate, but it means the check reports up even when the
certificate is invalid or expired.
Request metadata. Both maps are sent as gRPC request metadata on the health
RPC, which is what lets you check a health endpoint sitting behind an
authenticating proxy. metadata stays in the public, searchable configuration;
secretMetadata is encrypted at rest and never returned by the API. Keys must be
lowercase (gRPC lowercases them on the wire) and use only letters, digits, -,
. or _; the grpc- prefix is reserved by the gRPC runtime and -bin keys
(binary metadata) are not supported.
Metrics. The connection is established and timed phase by phase rather than being absorbed by the first RPC:
| Metric | Meaning |
|---|---|
dns_time_ms | Name resolution. Absent for a literal IP and for a tunneled check (which resolves on the far side of the bastion) |
connect_time_ms | TCP connection |
tls_time_ms | TLS handshake. Absent for a plaintext (h2c) check |
rpc_time_ms | The health RPC itself |
total_time_ms | End to end |
A failing check names the phase it died in (dns, connect, tls-handshake or
rpc), so a name that does not resolve, a refused port, a broken TLS handshake
and an unhealthy service are no longer the same error message.
The keyword / invertKeyword options match a substring of the serving-status
enum (SERVING / NOT_SERVING), which the serving-status check already covers.
They still work for checks that set them, but they are not offered in the
dashboard and should not be used for new checks.
Kafka
Monitor Apache Kafka broker connectivity.
URL Format:
kafka://hostname:9092
| Option | Description | Example |
|---|---|---|
| Broker | Kafka broker address | kafka:9092 |
| Timeout | Connection timeout | 10s |
RabbitMQ
Monitor RabbitMQ, in one of two modes.
AMQP mode (the default) connects over the AMQP protocol itself and, optionally, inspects a queue's depth and consumer count:
| Option | Description | Example |
|---|---|---|
| Host | RabbitMQ hostname | rabbitmq.example.com |
| Port | AMQP port | 5672 |
| Username / Password | AMQP credentials | guest / guest |
| Virtual Host | AMQP vhost | / |
| Queue | Queue to inspect (optional) | my-queue |
| TLS | Connect over amqps:// | |
| Timeout | Connection timeout | 10s |
Management mode talks to RabbitMQ's HTTP management API instead. It keeps
the same backward-compatible up/down probe (GET /api/health/checks/alarms
— down the moment RabbitMQ's own resource alarm is already active and
blocking publishers), and adds an early-warning layer on top: it also reads
each node's memory and disk figures from GET /api/nodes and can grade them
against two-tier warning/critical thresholds — so a check can page before
RabbitMQ's own watermark trips, not only once it already has.
| Option | Description | Example |
|---|---|---|
| Host | RabbitMQ hostname | rabbitmq.example.com |
| Management Port | Management API port | 15672 |
| Username / Password | Management API credentials | guest / guest |
| Memory used warning/critical | Ceiling on memory used: a percentage of RabbitMQ's high watermark, or a byte size | 80%, 1.5GiB |
| Disk free warning/critical | Floor on free disk: a byte size only | 10GiB |
| Timeout | Connection timeout | 10s |
Memory accepts a percentage because RabbitMQ reports both mem_used and the
high watermark (mem_limit) per node, so "80% of the watermark" is a
computable ratio. Disk accepts a byte size only: the management API reports
disk_free and the low watermark but never the volume's total size, so
there is no denominator to compute a percentage of.
In a cluster, the worst node decides the check's status: a critical breach on any node fails the check, a warning breach (with no critical breach anywhere) puts it in the amber warning state, which counts as up and never opens an incident. Metrics are recorded on every management-mode execution, with or without thresholds configured, so a check gains memory/disk history for free.
Thresholds are only accepted in management mode — AMQP has no visibility into a node's resource usage.
MQTT
Monitor MQTT broker connectivity via subscription.
URL Format:
mqtt://hostname:1883
mqtts://hostname:8883 # With TLS
| Option | Description | Example |
|---|---|---|
| Host | MQTT broker | mqtt.example.com |
| Port | MQTT port | 1883 |
| Timeout | Connection timeout | 10s |
Telephony
SIP (VoIP)
Monitor SIP servers used for voice/VoIP, either by checking reachability with an OPTIONS ping or by verifying that a user can register (REGISTER with digest authentication).
Address Format:
host:port
udp://host:5060
tcp://host:5060
tls://host:5061
| Option | Description | Default |
|---|---|---|
| Host | SIP server hostname or IP | - (required) |
| Port | SIP port | 5060 (UDP/TCP), 5061 (TLS) |
| Transport | udp, tcp, or tls | udp |
| Mode | options (ping) or register (auth) | options |
| Domain | SIP domain for From/To headers | same as Host |
| Username | SIP username (required for register) | - |
| Password | SIP password for digest auth (register) | - |
| Expect Status | Accepted SIP status codes for OPTIONS (e.g. 200,405) | - |
| TLS Verify | Verify the server certificate (TLS transport) | false |
| Timeout | Request timeout (max 60s) | 5s |
- OPTIONS mode succeeds when the server returns a valid SIP status code (matching
expect_statusif set). - REGISTER mode performs the standard two-step challenge/response and succeeds only on a final
200 OK.
NTP (Time Server)
Monitor an NTP time server. Unlike a plain UDP/123 reachability probe, this checker sends a real NTP request and judges the server as a clock: it confirms the server returns a valid response and reports itself healthy (stratum, leap indicator, and root distance, via the server's own self-report — no trust in the worker's clock). Two optional, opt-in thresholds let you also alert on the measured clock offset and on the server's stratum depth.
| Option | Description | Default |
|---|---|---|
| Host | NTP server hostname or IP | - (required) |
| Port | NTP UDP port | 123 |
| Version | NTP protocol version (3 or 4) | 4 |
| Timeout | Query timeout (max 60s) | 5s |
| Offset warn (ms) | Mark warning when the absolute clock offset exceeds this. 0 = off | off |
| Offset critical (ms) | Mark down when the absolute clock offset exceeds this. Must be ≥ Offset warn. 0 = off | off |
| Max stratum | Mark down when the server's stratum exceeds this (1–15). 0 = off | off |
- Default verdict = reachable and the server reports a usable clock. A Kiss-o'-Death (stratum 0), an unsynchronized server (stratum 16), a
LeapNotInSyncleap indicator, or an out-of-range root distance all yield down. - Clock offset is measured relative to the worker's own clock. A worker whose clock is itself skewed will report a misleading offset, so the offset thresholds are opt-in rather than the default verdict — keep this in mind when running across a distributed worker fleet.
- Metrics exposed: clock offset, RTT, stratum, root delay, root dispersion, root distance, poll interval, and precision.
NTP uses outbound UDP port 123, which is frequently blocked by egress firewalls. A blocked path surfaces deterministically as down/timeout.
Infrastructure
SNMP
Monitor devices via SNMP protocol.
| Option | Description | Example |
|---|---|---|
| Host | Target device | switch.example.com |
| Community | SNMP community string | public |
| OID | Object identifier | 1.3.6.1.2.1.1.1.0 |
| Version | SNMP version | 2c |
| Timeout | Connection timeout | 10s |
Docker
Monitor remote Docker daemon connectivity.
URL Format:
docker://hostname:2375
| Option | Description | Example |
|---|---|---|
| Host | Docker daemon | docker.example.com |
| Port | Docker API port | 2375, 2376 (TLS) |
| TLS | Enable TLS | true / false |
| Timeout | Connection timeout | 10s |
A2S Game Server (Source / Steam)
Query Source-engine and Steam game servers using the Valve A2S protocol (A2S_INFO).
| Option | Description | Example |
|---|---|---|
| Host | Game server address | game.example.com |
| Port | Query port | 27015 |
| Timeout | Connection timeout | 10s |
Minecraft
Monitor Minecraft servers (both Java and Bedrock editions), with optional player-count thresholds.
| Option | Description | Default |
|---|---|---|
| Host | Server hostname or IP | - (required) |
| Port | Server port | 25565 (Java), 19132 (Bedrock) |
| Edition | java or bedrock | java |
| Min Players | Alert if fewer players are online (0 = off) | 0 |
| Max Players | Alert if more players are online (0 = off) | 0 |
| Timeout | Query timeout (max 30s) | 10s |
The check reports online players, max players, MOTD, and version. It fails if the query fails or the player count falls outside the configured bounds.
Freebox Line (xDSL / FTTH)
Monitor the quality of a Freebox broadband line (xDSL or FTTH) through the Freebox OS API. The check connects via a stored Freebox integration connection rather than a direct address.
| Option | Description | Default |
|---|---|---|
| Connection | Reference to a freebox integration connection | - (required) |
| Link Type | xdsl or ftth | - (required) |
| Min Sync Rate (down) | Minimum downstream sync rate, kbps (xDSL) | 0 (off) |
| Min SNR Margin (down) | Minimum downstream SNR margin, dB (xDSL) | 0 (off) |
| Max Attenuation | Maximum downstream attenuation, dB (xDSL) | 0 (off) |
| Max CRC Errors | Maximum CRC errors per run (xDSL) | 0 (off) |
| Min / Max RX Power | Optical receive power bounds, mW (FTTH) | 0 (off) |
The check reports sync rates, SNR, attenuation, CRC counts (xDSL) or optical power and SFP details (FTTH), and fails when the WAN is down, the link is not trained, or any configured threshold is violated.
Kubernetes (Workload Replica Health)
Monitor a Kubernetes workload's replica health — the structural analog of how the Docker check mirrors a container's HEALTHCHECK. The check connects via a stored Kubernetes cluster connection (an integration of type kubernetes) referenced by UID, never an inline credential.
| Option | Description | Default |
|---|---|---|
| Cluster | Reference to a kubernetes integration connection | - (required) |
| Namespace | Workload namespace | - (required) |
| Kind | Deployment or ReplicaSet | - (required) |
| Name | Workload name | - (required) |
| Timeout | Per-execution API timeout (max 60s) | 10s |
Status semantics (ready vs. desired replicas):
- Up —
readyReplicas == desiredReplicasanddesiredReplicas > 0. - Warning —
0 < readyReplicas < desiredReplicas(mid-rollout or partially degraded), ordesiredReplicas == 0(intentionally scaled to zero — surfaced, not paged). - Down —
readyReplicas == 0withdesiredReplicas > 0, a stuck rollout (DeploymentProgressing=False/ProgressDeadlineExceeded), or the workload no longer exists.
Outputs include the namespace, kind, name, container images, and workload conditions; metrics include desiredReplicas, readyReplicas, availableReplicas, updatedReplicas, and unavailableReplicas.
Cluster connection
Register a cluster once under Integrations → Kubernetes (it is a data source, not a notification channel). Three authentication modes:
- API server + token — an API server URL plus a bearer token (typically a service-account token), optionally a CA certificate (or skip TLS verification).
- Kubeconfig — paste a full kubeconfig that resolves to an API server and credentials.
- In-cluster — when SolidPing itself runs as a pod in the target cluster, it uses the mounted service-account token; no credentials are stored.
The token / kubeconfig is stored encrypted (AES-256-GCM) in the connection's private settings and is never returned to the dashboard. Use Test connection to confirm the credentials work — it calls the cluster's /version endpoint.
Required RBAC
The connection only needs read access to the monitored workloads. Bind a read-only ClusterRole to the service account whose token you register:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: solidping-readonly
rules:
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: solidping-readonly
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: solidping-readonly
subjects:
- kind: ServiceAccount
name: solidping
namespace: solidping
Kubernetes discovery (enumerating workloads automatically) builds on this same connection and additionally needs
get/listonservices,endpoints, andingresses(andnodes); grant those too if you plan to use it.
Metrics
Prometheus Metric
Alert on a number a target reports about itself rather than on whether it
answers. Every other check type answers "is it up?"; this one answers "is the
value still acceptable?" — a queue depth, a free-disk gauge, an open-file-descriptor
count, an error rate. It works against any endpoint that speaks the Prometheus
text exposition format (most Go, Java, Python and Rust services expose one at
/metrics), and against a Prometheus server itself via PromQL. No Prometheus +
Alertmanager stack is required.
| Option | Description | Default |
|---|---|---|
| URL | Metrics endpoint (scrape) or Prometheus server base URL (promql) | - (required) |
| Mode | scrape or promql | scrape |
| Metric | Series name to select (scrape mode) | - (required in scrape) |
| Labels | Label/value pairs the series must carry (subset match, scrape mode) | none |
| Query | PromQL instant query (promql mode) | - (required in promql) |
| Operator | >, >=, <, <=, ==, != | > |
| Warning Value | Threshold that yields Warning (amber, counts as up, never pages) | unset |
| Critical Value | Threshold that yields Down (pages) | unset |
| Match | What to do when several series match: single, min, max, sum, avg | single |
| On Missing | Status when nothing matches: down, warning, up | down |
| Headers | Extra request headers (bearer / basic auth) | none |
| Timeout | Per-execution request timeout (max 60s) | 15s |
At least one of Warning Value / Critical Value must be set. 0 is a
perfectly legal threshold — "alert when free slots reach 0" is written as
warningValue: 0, and it is honoured as a real threshold, not read as "unset".
Modes
scrape fetches the URL, parses the exposition format, and picks the series
named by Metric whose labels contain every configured label/value pair
(extra labels on the series are fine). Histograms and summaries need no special
handling: address them through the flattened series names Prometheus itself
exposes — <name>_sum, <name>_count, <name>_bucket (with an le label), or
<name> with a quantile label.
{
"url": "https://app.example.com/metrics",
"mode": "scrape",
"metric": "process_open_fds",
"labels": { "instance": "app-1" },
"operator": ">",
"warningValue": 800,
"criticalValue": 1000
}
promql treats the URL as a Prometheus server base URL and runs an instant
query against {url}/api/v1/query. Scalar and instant-vector results are
accepted; a matrix (range) result is rejected — a range returns a series of
points over time, not one current value, so use an instant query.
{
"url": "https://prometheus.example.com",
"mode": "promql",
"query": "sum(rate(http_requests_total{code=~\"5..\"}[5m]))",
"operator": ">",
"warningValue": 1,
"criticalValue": 10
}
Threshold semantics
The check fires when value <operator> threshold is true:
- Critical breached → Down (pages, opens an incident).
- Otherwise warning breached → Warning (amber; counts as up for availability, aggregates to Degraded, does not open an incident).
- Otherwise Up.
A check with only a Warning Value is valid and can never page — the same
contract as warningDays on the Domain and
SSL checks. Use it for "I want to see this, not be woken
by it".
The operator points the thresholds. For > / >= (something growing too big),
Critical must be greater than or equal to Warning. For < / <= (something
shrinking too far) the ordering reverses — this is the free-disk shape:
{
"url": "https://app.example.com/metrics",
"mode": "scrape",
"metric": "node_filesystem_avail_bytes",
"labels": { "mountpoint": "/" },
"operator": "<",
"warningValue": 10737418240,
"criticalValue": 2147483648
}
That check warns below 10 GiB free and pages below 2 GiB. == and != accept a
Critical Value only — there is no ordering between two equality targets, so a
warning tier would be meaningless and is rejected at validation time.
Multiple matching series
By default (match: single) a selector that matches more than one series is an
error result, not a silent pick — an ambiguous selector is a configuration bug,
and guessing would grade an arbitrary series. Either narrow the selector with
labels, or choose an explicit aggregation: min, max, sum or avg.
Missing metrics
onMissing decides what an absent metric (or an empty PromQL vector) means.
The default is down: a metric that vanished usually means the target is
broken, not healthy. Use warning when the metric is expected to be absent
occasionally, or up when its absence is genuinely fine.
Counters and rates
This check reads whatever value is exposed, right now. It performs no
client-side rate computation — a rate needs state between executions, which is
explicitly out of scope. A raw counter (http_requests_total) only ever grows,
so thresholding it directly is rarely what you want. For rates, use promql
mode and let Prometheus do the work: rate(http_requests_total[5m]).
Graphing
Every execution records the resolved value as a value metric, so the check
page graphs the monitored number over time and rolls it up like any latency
metric — that history is half the point of the check type.
Limits
Scrape responses are capped at 5 MB. A larger body is refused with an
explicit error naming the cap — never truncated: a half-read exposition body
still parses, and would then be graded as a wrong-but-plausible value. If your
/metrics output is that big, narrow the endpoint or switch to promql mode,
where the query bounds the response.
Special Check Types
Heartbeat
Passive monitoring that expects incoming pings at regular intervals. Instead of SolidPing checking a service, the service pings SolidPing to report it's alive.
A device with no room for a TLS stack can push the same heartbeat over raw TCP or UDP, one line per beat, optionally HMAC-signed and carrying battery volts or RSSI as metrics. See Embedded devices (TCP/UDP).
| Option | Description | Default |
|---|---|---|
| Period | Expected ping interval | 60s |
| Grace | Grace period before incident | 30s |
Use cases:
- Cron job monitoring
- Backup completion verification
- Batch process health checks
- IoT device connectivity
Each ping's caller metadata (User-Agent header, source IP, and HTTP method) is recorded and shown on that ping's result detail page — useful for confirming which script or host is actually pinging the check.
Two kinds of result rows
A heartbeat check's result history interleaves two kinds of row, and they mean different things:
| Beat (signal row) | Scheduler evaluation | |
|---|---|---|
| Written when | your caller pings the check | every period, by a checks worker |
| Written by | the heartbeat endpoint | the scheduler |
| Region | none | the worker's region |
| Output keys | message, plus userAgent / remoteAddr / httpMethod / data when the caller supplied them | evaluation: true, lastSignalAt, lastSignalResultUid, plus overdueBy or runStarted where they apply |
evaluation: true is the reliable way to tell them apart. An ingested beat
never carries it, so its absence means the row is a real signal. In the
dashboard, evaluation rows carry a muted "Evaluation" badge in the Recent
Results table and a "Scheduler evaluation" card — with a link to the beat they
looked at — on the result detail page.
So a ping usually produces two rows within seconds of each other: your beat, and the scheduler's evaluation confirming it arrived on time. That second row having no caller metadata is expected — nothing called in at that moment.
Evaluation messages:
| Situation | Message |
|---|---|
The last beat was up and arrived within the period | Heartbeat on time |
The last beat was up but is now older than the period | Heartbeat overdue |
The last beat reported failure (?status=down) | Last heartbeat reported failure |
The last beat reported an error (?status=error) | Last heartbeat reported error |
A running beat is still inside the 2× period grace window | Run in progress |
A running beat exceeded that window | Run started but never completed |
| No beat on record at all | No heartbeat received |
The evaluation reads the newest stored beat. Raw results are kept for a limited
window (24 h by default) before being rolled up, so a check that has been silent
for longer than that has no beat left to point at and its evaluations read
No heartbeat received — the same message a check that was never pinged gets.
The keys above are part of the documented API surface: anything reading
output from GET /api/v1/orgs/{org}/results or through the MCP server
(list_results, diagnose_check) sees them, and should branch on
evaluation rather than on the message text.
Sending the token. The dashboard generates a ?token= URL that works
everywhere, but the token can also travel as an Authorization: Bearer
header instead — useful when you'd rather not put a secret in a URL (proxy
and CDN access logs, shell history, Referer headers). Both forms are
accepted forever; if a request supplies both, the header wins.
# Query string (works everywhere, including a bare browser tab)
curl "https://your-solidping.example.com/api/v1/heartbeat/default/my-cron-job?token=<TOKEN>"
# Authorization header (keeps the token out of logs and URLs)
curl -H "Authorization: Bearer <TOKEN>" \
"https://your-solidping.example.com/api/v1/heartbeat/default/my-cron-job"
Structured body. A JSON body's message key still becomes the ping's
message exactly as before. A durationMs key — a number of milliseconds,
between 0 and 604 800 000 (7 days) — becomes the result's response time,
feeding the check's response-time chart just like an active probe's
measured duration; an invalid value (wrong type, negative, or over the cap)
is ignored for that purpose but still shown in the "Data" card below, so you
can see exactly what was sent. Any other keys in the body are stored
alongside it and shown in that "Data" card on the result detail page — handy
for a CI run URL, commit SHA, record count, or batch ID. The body is capped
at 8 KiB; malformed JSON is tolerated (the ping is still recorded with an
empty message), but an over-cap body is rejected with 400.
curl -X POST -H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"message":"backup completed","durationMs":42000,"recordCount":18234,"runUrl":"https://ci.example.com/runs/512"}' \
"https://your-solidping.example.com/api/v1/heartbeat/default/my-cron-job"
Monitoring GitHub Actions
Your nightly workflow can stop running and nobody notices. GitHub already
emails you when a workflow fails, but it has no way to tell you a scheduled
workflow never ran at all — and GitHub auto-disables scheduled workflows
after 60 days of repository inactivity, silently. A broken cron expression,
a renamed default branch, or a deleted secret can just as easily stop a
schedule from firing, with zero notifications either way. This is exactly the
gap a heartbeat check closes: period + grace is an assertion about
absence — "if no ping arrives in time, open an incident" — which no
notify-on-failure system can make.
fclairamb/solidping-action
wraps the ping in one step, mapping the job's outcome to the right heartbeat
status and building an actionable message (run URL, workflow name, run
number, commit SHA, actor) from the github context:
- uses: fclairamb/solidping-action@v1
if: always()
with:
org: acme
check: nightly-backup
token: ${{ secrets.SOLIDPING_HEARTBEAT_TOKEN }}
status: ${{ job.status }}
status should always be ${{ job.status }}; the action maps it to
SolidPing's vocabulary:
job.status | Heartbeat status |
|---|---|
success | up |
failure | down |
cancelled | no ping sent |
| anything else | error |
This is for on: schedule workflows only — not push-triggered CI. A
heartbeat's period is meaningful for a cron job that's expected to run
every N minutes; a push-triggered job has no period, so a quiet repo with no
pushes for a few days would trip the grace window and page someone for
nothing. Reporting push-triggered CI failures is a different, useful feature,
but it isn't this one.
JavaScript
Custom monitoring scripts with arbitrary logic, run against a real sandboxed JS engine once per execution: log in through a form and hold a session, chain a bearer-token login into an authenticated call, or aggregate several checks into one result.
Minimum period: 30s (default 1m) — see Check Intervals.
Use cases:
- Complex multi-step API workflows
- Custom business logic validation
- Conditional checks based on time or state
- Aggregating multiple checks into one
- Login flow verification — a
jscheck can drive a real headless-Chrome page (fill the form, click, assert on what rendered) through itsbrowserAPI; a script that opens one runs at the browser check's1mfloor
Where a credential goes, how http and http.session() work, the full
solidping.*/base64/browser/console API, and full tested examples
(bearer-token chaining, a cookie-jar login, a real-browser form login, Basic
auth, cleanup workflows, sub-check aggregation) are on their own page:
JavaScript checks →
Browser
Headless browser-based monitoring using a real browser engine.
Minimum period: 60s (default 5m) — a headless-browser run costs several
seconds, so faster periods would occupy a monitoring slot continuously. See
Check Intervals.
Use cases:
- Single-page application monitoring
- Visual regression detection
- JavaScript-rendered content checks
A browser check loads a page: navigate, optionally wait for a selector,
optionally match a keyword. It has no click and no typing. To drive the page
— fill a form, click, assert on the result — write a
js check and use its browser API.
Where Chrome comes from
The SolidPing image is distroless and deliberately ships no browser — a browser check drives a Chrome that lives outside the SolidPing process. There are two ways to give it one, and nothing is ever downloaded at runtime.
1. Remote Chrome over CDP (recommended, and the only option in containers). Point the worker at a long-lived headless Chrome speaking the Chrome DevTools Protocol:
| Setting | Environment variable | Meaning |
|---|---|---|
checkers.browser.cdp_url | SP_CHECKERS_BROWSER_CDP_URL | Websocket/HTTP address of the CDP endpoint, e.g. ws://browser:9222 |
checkers.browser.chrome_path | SP_CHECKERS_BROWSER_CHROME_PATH | Local Chrome binary for the fallback below |
Each execution opens a fresh isolated (incognito) browser context and tab, torn down afterwards, so consecutive checks never share cookies, storage or a service worker. Because the browser process is already running, the measured duration no longer includes Chrome's ~1s cold start.
The expected deployment shape is a sidecar next to each checks worker —
chromedp/headless-shell, pinned to a tag so every region runs the same Chrome
version, reached over localhost. In Docker Compose that is an extra service; see
Docker Compose installation.
In Kubernetes it is a second container in the worker Pod with
SP_CHECKERS_BROWSER_CDP_URL=ws://127.0.0.1:9222.
If the endpoint is unreachable, the check reports an error — never "down".
Your monitored site is not implicated by your browser sidecar being down, and
SolidPing will not raise a false incident for it. The browser capability
(below) also drops on the next worker heartbeat.
The same sidecar now serves js checks too: a script's
browser API opens its page through this
endpoint, under the same slot cap and the same isolated context. Give every
region that runs js checks one, or pin browser-using scripts to regions whose
capability list shows browser — a js check is scheduled as a js check and
is not routed to a browser-capable region for you.
2. Local Chrome binary (fallback). With no cdp_url configured, SolidPing
executes a locally installed Chrome/Chromium: chrome_path if set, otherwise
the usual binary names (google-chrome, chromium, chromium-browser, …).
This is the zero-config path on a developer laptop and an opt-in for agents
installed on a host that already has Chrome. SolidPing never downloads a
browser.
Concurrency
At most 4 browser executions run at a time per worker — a browser execution
costs orders of magnitude more than a network probe, and an unbounded pool of
tabs would starve the sidecar. A fifth execution waits for a slot inside its own
timeout budget and reports a timeout if none frees up. A js script holding a
page counts as one of the four, for as long as it holds it. Space browser checks out,
or add workers, rather than lowering their period.
Region capability
Workers self-report a browser capability (a reachable CDP endpoint, or a local
binary), aggregated per region alongside ipv4/ipv6. Creating or editing a
browser check in a region whose live workers report no browser produces a
warning naming a region that does have one — the check is still saved and
still runs. A region with no live worker, or served by an agent predating the
probe, reports unknown and never warns.
Common Options
All check types support these common options:
| Option | Description | Default |
|---|---|---|
name | Display name | - |
description | Description | - |
enabled | Enable/disable check | true |
period | Check interval | 60s |
timeout | Request timeout | 30s |
regions | Worker regions to run from | all |
incident_threshold | Failures before incident | 1 |
escalation_threshold | Failures before escalation | 3 |
recovery_threshold | Successes before recovery | 1 |
ipVersion | Address family to probe over (auto, ipv4, ipv6) | auto |
IP version
An IPv4 check does not verify IPv6 reachability. By default a check resolves its target and probes exactly one address — whichever family it lands on first. So a dual-stack host whose IPv6 path is broken (a missing AAAA record, a firewall rule that was never added for v6, a dead v6 route, a load balancer listening on v4 only) keeps reporting up while every IPv6 user is down.
Set ipVersion to pin the check to a family, or pick it under Advanced → IP
version in the dashboard:
| Value | Meaning |
|---|---|
auto (default) | No constraint — probe one address, exactly as before this option existed. Unchanged behaviour for every check that does not set it. |
ipv4 | Probe over IPv4 only. Fails if the target publishes no A record. |
ipv6 | Probe over IPv6 only. Fails if the target publishes no AAAA record. |
The family a probe actually used is reported back as the ip_version field of
the result output, and shown on the check detail page — so an ipVersion: ipv6
check that reports ip_version: ipv6 is real, verified IPv6 coverage.
One check covers one family. auto means "pick one", not "probe both" — this
is a deliberate difference from Better Stack, where an unset value monitors both.
To cover both families, create two checks on the same target, one pinned to each;
the Better Stack importer warns when a monitor relied on that
default.
Supported on http, tcp, udp, icmp, ssl, ssh, smtp, imap, pop3
and dnsbl (where only auto/ipv4 are accepted — DNS blocklists are indexed
by IPv4 address). The dashboard only shows the option on types that support it,
driven by supportsIpVersion on /api/v1/orgs/{org}/check-types.
dns checks do not take ipVersion and reject it. For a DNS check the
option could mean either "which record types to assert on" or "which transport to
reach the nameserver over" — two different features. Use the dns check's own
record_type (A / AAAA) to assert on records.
Tunneled checks do not take ipVersion either and reject the pair. A
tunneled check is resolved and dialed on the far side of the
bastion, so the address family is the tunnel's to choose — pinning it here could
only ever be a claim the worker cannot honor.
When a worker has no IPv6. A check pinned to a family the worker itself cannot originate fails with an explicit error saying so — it names the worker's missing egress rather than blaming the target. If you see it, change the check's region rather than investigating your service.
SSH tunnel
Every TCP-based check type can dial its target through an
SSH check's bastion — set tunnelCheckUid to a reference to
an ssh check that has expected_fingerprint set, or pick it under Advanced →
Run through SSH tunnel in the dashboard. This covers the classic bastion use
cases (a database or broker on a private network): http, tcp, ssl,
websocket, grpc, postgresql, mysql, mssql, oracle,
clickhouse, redis, mongodb, rabbitmq, kafka, mqtt, smtp, imap, pop3, and ftp.
A js check can tunnel
too: its http.* and socket handles are dialed through the bastion, but a
sub-check of an unlisted type or browser.open() is refused rather than run
from the worker's own network.
UDP- and ICMP-based types (icmp, udp, ntp, snmp, dns, dnsbl, sip,
a2s) cannot tunnel — an SSH direct-tcpip forward is TCP only. The dashboard
only shows the option on types that support it, driven by supportsTunnel on
/api/v1/orgs/{org}/check-types.
Check Intervals
Supported interval formats:
- Seconds:
10s,30s,60s - Minutes:
1m,5m,15m - Hours:
1h,6h,24h,168h(1 week),336h(2 weeks),720h(30 days)
The dashboard's interval picker offers 1 week / 2 weeks / 30 days for any
check type whose MaxPeriod allows it (uncapped by default — see the table
below), useful for slow-moving checks like domain expiration. Every region
you select still runs the check at the full interval — see
Multiple regions.
Minimum intervals
The API enforces a minimum period per check type — both in the dashboard and
on direct API calls (400 VALIDATION_ERROR naming the floor):
| Check type | Minimum period | Default period |
|---|---|---|
browser | 60s | 5m |
js | 30s | 1m |
ssl | 1h | 6h |
domain | 6h | 24h |
dnsbl | 15m | 1h |
| All other types | 10s | 1m |
Heavy check types (headless browser, custom scripts) carry higher floors because each run can occupy a monitoring slot for several seconds — a fast period would keep a runner busy full-time. Existing checks are grandfathered: a period created before a floor was raised keeps working, and the limit applies on the next edit. The check detail page shows a warning when a check occupies 50% or more of a runner slot (its duty cycle).
Recommended minimum: 30s for production.
Multiple regions
The period applies per region: each region you select runs the check at
the full interval, and SolidPing staggers the regions across the period so they
don't all fire at once. A 60s check on 3 regions runs every 60 seconds in
each region (roughly 20 seconds apart), for a combined detection interval of
about 20 seconds — selecting more regions multiplies coverage, it does not
divide the frequency.
By default the inter-region offset ("spread") is period ÷ region count. Set
regionSpread (a duration, API-first) to override it — e.g. 1s to sample all
regions almost simultaneously for comparative cross-region latency, or 0s to
fire them together. It must satisfy 0 ≤ regionSpread < period.
Because every region executes independently, a multi-region check consumes
regions × 60s ÷ period checks per minute against your plan's
checks-per-minute limit (a 60s check on 3 regions counts as 3/min). The
Usage page reflects this multiplier.
Best Practices
- Use appropriate timeouts - Set timeouts based on expected response times
- Avoid excessive frequency - Sub-minute checks increase load on both SolidPing and targets
- Use meaningful names - Make check names descriptive for quick identification
- Set appropriate thresholds - Balance between noise and missing real issues
- Monitor from multiple regions - Use distributed workers for global services
- Use database checks for actual connectivity - Prefer native database checks (PostgreSQL, MySQL, Redis) over generic TCP checks for database monitoring
- Leverage heartbeat checks - Use heartbeat mode for monitoring cron jobs and batch processes