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
| Rule | Detail |
|---|---|
| Names | Letters, digits, _, and -, up to 63 characters. Include the unit, as in disk_free_mib |
| Case | Stored in lowercase. Assertions must use lowercase too |
| Values | Zero, negative, and decimal numbers. NaN and Inf are dropped |
| Limit | 10 custom names per event. Only the first 12 custom metric parameters are read, including invalid or repeated ones |
| Repeats | The last valid value wins |
| Format | One 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 when | Assertion |
|---|---|
| A disk is nearly full | metric.disk_used_pct < 90 |
| A job did no useful work | metric.rows_imported > 0 |
| CPU stays high, ignoring brief spikes | metric.cpu_used_pct.mean < 90 over 15 minutes |
| Your slowest requests get slower | metric.sync_latency_ms.p95 < 500 over 1 hour |
| Daily sends pass a budget | metric.emails_sent.sum < 50000 over 1 day |
| Daily throughput falls too low | metric.rows_imported.sum > 10000 over 1 day |
- Aggregates:
sum,mean,min,max,p50,p90,p95, andp99. Percentiles (p50,p90,p95,p99) are approximate. - Operators:
<,<=,=,>,>=, and!=. - Periods: Such as
15 minutes,24h, or7 days, at most 90 days. Use a period longer than the time between samples: a nightly job needs at leastover 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 explicit0is a real sample, sometric.rows_imported > 0fires. 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, orfailevent. 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, andmetric.error_ratedon't support!=.metric.durationsupports only<,<=,>, and>=.metric.error_rateiserror_count / counton the latest event, written as a decimal like0.01. Don't send a custom metric namederror_rate: this assertion ignores it.- Windows on
durationandlatencyaccept time values, as inmetric.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.