Cronitor API

Custom Metrics

A job can finish on time and still import zero rows. A server can keep sending heartbeats while its disk fills up. Custom metrics report the numbers that show whether the work is healthy, so Cronitor can chart them and alert you when one goes out of range.

Custom metrics work on job and heartbeat monitors. There's nothing to set up: send a number with a new name and Cronitor records it.

Quick start

1. Send a metric by adding metric=name:value to a telemetry request. Use an API key with the monitor:telemetry permission.

curl --fail --get "https://cronitor.link/p/YOUR_API_KEY/web-01" \
  --data-urlencode "metric=disk_used_pct:63"

2. View the chart. Open the monitor in the dashboard. Heartbeats show the Metrics chart once a metric arrives. On a job, choose Metrics from the chart selector.

3. Add an alert. Edit the monitor, choose Add Metric Assertion, and enter what healthy looks like. This one alerts at 90 or more:

metric.disk_used_pct < 90

Send metrics

Repeat metric for each value. Jobs should send metrics on complete, when the results are known:

curl --fail --get "https://cronitor.link/p/YOUR_API_KEY/nightly-import" \
  --data-urlencode "state=complete" \
  --data-urlencode "metric=rows_imported:48210" \
  --data-urlencode "metric=rows_rejected:12"

Heartbeats need no state. Metrics can also be sent on run and fail events. Each event is a separate sample, so sending a value on both run and complete doubles its sum.

SDKs and CronitorCLI send the same data:

cronitor.Monitor('web-01').ping(metrics={'disk_used_pct': 63, 'memory_used_pct': 44})
new cronitor.Monitor('web-01').ping({ metrics: { disk_used_pct: 63, memory_used_pct: 44 } });
Cronitor::Monitor.new('web-01').ping(metrics: { disk_used_pct: 63, memory_used_pct: 44 })
cronitor ping web-01 --tick --metric "disk_used_pct=63,memory_used_pct=44"

CronitorCLI and the Java SDK send whole numbers only, and CronitorCLI silently drops decimals. See the SDK docs for other languages.

Names and values

RuleDetail
NamesLetters, digits, _, and -, up to 63 characters. Include the unit, as in disk_free_mib
CaseStored in lowercase. Assertions must use lowercase too
ValuesZero, negative, and decimal numbers. NaN and Inf are dropped
Limit10 custom names per event. Only the first 12 custom metric parameters are read, including invalid or repeated ones
RepeatsThe last valid value wins
FormatOne value per parameter: metric=a:1,b:2 is dropped. name=value also works

Invalid metrics are dropped silently. The HTTP response means the ping was received, not that every metric was stored. To confirm a metric arrived, expand the event in the monitor's activity list.

Read the chart

The chart draws up to 10 metrics, most recently seen first, as the average per hour, or per day for ranges over 14 days. The tooltip shows the sample count.

All lines share one Y axis, so a large metric can flatten a small one. Select a name in the legend to hide or show its line.

A gap means no samples arrived. To show a real zero, send 0.

Alert on metrics

An assertion states what healthy looks like, and Cronitor alerts when a value breaks it. There are two forms:

metric.<name> <operator> <value>
metric.<name>.<aggregate> <operator> <value> over <period>
  • Latest value checks the most recent event. Use it when one bad reading matters.
  • Window combines samples from a recent period. Use it for noisy values, totals, and percentiles.
You want to know whenAssertion
A disk is nearly fullmetric.disk_used_pct < 90
A job did no useful workmetric.rows_imported > 0
CPU stays high, ignoring brief spikesmetric.cpu_used_pct.mean < 90 over 15 minutes
Your slowest requests get slowermetric.sync_latency_ms.p95 < 500 over 1 hour
Daily sends pass a budgetmetric.emails_sent.sum < 50000 over 1 day
Daily throughput falls too lowmetric.rows_imported.sum > 10000 over 1 day
  • Aggregates: sum, mean, min, max, p50, p90, p95, and p99. Percentiles (p50, p90, p95, p99) are approximate.
  • Operators: <, <=, =, >, >=, and !=.
  • Periods: Such as 15 minutes, 24h, or 7 days, at most 90 days. Use a period longer than the time between samples: a nightly job needs at least over 25 hours.

Add an assertion

In the dashboard, edit the monitor, choose Add Metric Assertion, and type the expression after the metric. prefix. With the Monitors API:

curl --user API_KEY: \
  --request PUT \
  --header "Content-Type: application/json" \
  --header "Cronitor-Version: 2025-11-28" \
  --data '{
    "monitors": [
      {
        "type": "heartbeat",
        "key": "web-01",
        "schedules": ["every 5 minutes"],
        "assertions": [
          "metric.disk_used_pct < 90",
          "metric.cpu_used_pct.mean < 90 over 15 minutes"
        ]
      }
    ]
  }' \
  https://cronitor.io/api/monitors

assertions replaces the monitor's existing assertions, so send the full set. An assertion can be saved before its metric is first sent.

How alerts behave

Missing data doesn't alert. Missing data is skipped, never treated as zero:

  • To catch a job that did nothing, send 0. An explicit 0 is a real sample, so metric.rows_imported > 0 fires. If the job leaves the metric out, it can't.
  • To catch a job or heartbeat that stops reporting, give the monitor a schedule. Schedules cover silence, and assertions cover values.
  • A latest-value assertion only looks at the most recent run, complete, tick, or fail event. If that event lacks the metric, the assertion is skipped and older values aren't used.
  • An empty window is skipped too, and it can't resolve an open alert. The one exception is below.

A sum can be 0 without samples. If a complete or tick event arrives in the window without the metric, and the monitor reported that metric in the 90 days before the window, the sum is 0. This catches a job that keeps running but stops reporting a number, so metric.rows_imported.sum > 0 over 1 hour alerts. Other aggregates stay skipped, and so does a window with no complete or tick events at all.

One bad value is enough. failure_tolerance does not apply to metric assertions. If that's too sensitive, use a window.

Use one monitor per host. Latest-value assertions use the most recent event from any host, so readings from several servers mix.

Timing. Window results are cached for up to three minutes, and a new event refreshes them sooner. Windows longer than 7 days refresh on a timer only: about hourly for 30 days and every 3 hours for 90 days.

Recovery. A recovery on any assertion resolves the monitor's incident. If another metric assertion is still failing, it alerts again.

Shared history. Assertions with the same metric, aggregate, and window share one alert history, even with different thresholds. Editing a threshold keeps the existing failure state.

Built-in metrics

count, error_count, duration (seconds), latency, and length have built-in meanings and don't count toward the limit of 10. You send and assert on them the same way, with three differences:

  • metric.count, metric.error_count, and metric.error_rate don't support !=. metric.duration supports only <, <=, >, and >=.
  • metric.error_rate is error_count / count on the latest event, written as a decimal like 0.01. Don't send a custom metric named error_rate: this assertion ignores it.
  • Windows on duration and latency accept time values, as in metric.duration.p95 < 30s over 1 day. A bare number uses the metric's own unit.

Read metrics with the API

The Metrics API returns the data behind the chart. Pass metric=<name> for a series of averages and sample counts, or metricNames=true to list the names a monitor has reported:

curl "https://cronitor.io/api/metrics?monitor=web-01&time=24h&metric=disk_used_pct" -u API_KEY:

GET /api/monitors/:key and the MCP server also return metric names.

Troubleshooting

A metric isn't showing up. Check the naming rules. Invalid metrics are dropped without an error.

An assertion never fires. The latest event may not carry the metric, or the window may have no samples. Check recent events, and use a period longer than the time between samples.

An alert won't resolve. Recovery needs a new passing value. If the metric stopped arriving, the assertion is skipped and the alert stays open.

An assertion was rejected. A window needs an aggregate and over <period>, a latest-value assertion can't include over, and names must be lowercase.

Previous
Telemetry