---
title: "Run totals per monitor across the project"
method: GET
path: "/api/v1/runs/by-monitor"
tags: ["Events"]
---

# Run totals per monitor across the project

`GET /api/v1/runs/by-monitor`

## Request

- Base URL: `https://app.lastping.dev`
- URL: `https://app.lastping.dev/api/v1/runs/by-monitor`
- Auth: API key in header `X-Api-Key`

The same runs `GET /api/v1/runs` lists, grouped by monitor instead of
listed: how often each job ran inside a window, how often it
succeeded, how often it failed, and how long it takes on average.

**The outcome rule is the one rule.** `succeeded` and `failed` here
are computed by the same rule that decides `outcome` on
`GET /api/v1/runs` — a terminal `success`/`fail`/`cancel` ping
decides, an incident opened on a run with no terminal ping of its own
means `failed`, a last-seen `blocked` ping means `blocked`, and
anything else is still running or unfinished. So a success rate derived from these
numbers can never contradict the list a reader has just filtered, and
a cancelled, blocked, unfinished or in-flight run is counted in `runs` and in
neither `succeeded` nor `failed`.

A run that exists only as spans is counted too, graded as
`Run.traced` describes, and the set-up's test span is in no count.

**`?since=`** defaults to the last 30 days and is clamped to 365 days
back, exactly as `GET /api/v1/deliveries` is. The response echoes the
window it actually used in `since`, so a client labels the table from
the answer rather than from its own request.

**A monitor with no run inside the window is absent**, not a row of
zeroes. Read the absence as "this job did not run in this range".

There is no pagination: one row per monitor, bounded well above the
per-project monitor limit.

Not mirrored in MCP or Terraform, and that is a decision rather than
an omission: `get_run_history` already answers
the same question for one monitor when an agent asks, and Terraform
owns declarative configuration rather than rollups of what has
happened.

## Query parameters

- `monitor` string, uuid
- `since` string, date-time

## Response `200`

One row per monitor with runs in the window (may be empty), busiest first.

- RunsByMonitorPage: The body of `GET /api/v1/runs/by-monitor`: the window the server actually used, and one row per monitor that has a run inside it.
  - `since` string, date-time, required: The start of the window these rows describe, AFTER the default and the clamp were applied. Echoed so a client can label the table with the range the server used rather than the one it asked for.
  - `monitors` RunsByMonitorRow[], required: Ordered by `runs` descending, then by name. A monitor with no run inside the window is ABSENT rather than present with zeroes: "this job did not run" and "this job ran and never finished" are different facts.
    - `check_id` string, uuid, required
    - `check_name` string, required: The monitor's name. Third-party text: render it as text, never interpret it as markup.
    - `runs` integer, required: Every run of this monitor that started inside the window, whatever its outcome. **It is larger than `succeeded + failed` whenever the window holds a cancelled, blocked, unfinished or still-running run**, which are counted here and in neither of those. So it is the wrong denominator for a success rate: a monitor with ten runs, five succeeded and five still in flight would read 50%, which reads as "half of them broke". Divide by `succeeded + failed`, the runs that actually reached a verdict, and report no rate at all when that is zero — 0 of 0 is not 0%. That is the same treatment `avg_duration_ms` gives an unfinished run, which it excludes rather than counting as zero.
    - `succeeded` integer, required
    - `failed` integer, required: Includes a run an incident closed with no terminal ping of its own, exactly as the run list's own `failed` outcome does: both are computed by the same rule, so a rate derived here can never contradict that list.
    - `avg_duration_ms` integer, nullable, required: Mean wall-clock duration of the runs in this window that FINISHED, in milliseconds. **Null when none did** — never `0`. A run still in flight has no duration, and counting it as zero would drag the figure toward zero on exactly the monitors that are busiest right now. A finished run whose shape carries no measurement (a bare success ping with no start to measure from, and no `duration_ms` of its own) is excluded from both the sum and the divisor for the same reason.

## Other responses

- `400`: An invalid `monitor` or `since` (`INVALID_FILTER`).
- `401`: Missing or invalid API key.
- `429`: Too many requests. The per-principal fixed-window rate limit has been exceeded. Retry after the number of seconds indicated by the `Retry-After` response header. The problem body's `code` is `RATE_LIMITED`. It names this limiter specifically, so a client can tell it apart from the application-level throttles that also answer 429 with their own codes and their own, much longer, `Retry-After` values (`TEST_PING_RATE_LIMITED`, `TOO_MANY_ATTEMPTS`). Back off globally on `RATE_LIMITED`; back off on the one operation for the others. The principal is the API key for a key-authenticated request and the USER for a browser session, so every tab one person has open shares one budget.

---

[API](https://skmtc.dev/lastping/apis/lastping-management-api.md) · [All operations](https://skmtc.dev/lastping/apis/lastping-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lastping/apis/lastping-management-api/revisions/bbc6eda91c46?raw)
