> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmcpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The nightly pass

> Three metrics are computed after the fact at 02:00 UTC, alerts are evaluated at 02:15, and cleanup runs at 03:00. What that means for the numbers you read.

Thirteen of the sixteen metrics are counted live, as payloads arrive. Three are not:

* [**Retries**](/metrics/first-call-success)
* **First-call success**
* [**Tool pairs**](/metrics/tool-pairs)

All three are questions about a **sequence** rather than about a call, so none can be answered at the moment a call is recorded.

## What it does

At **02:00 UTC**, for the previous day, per MCP: walk each session's calls ordered by `started_at`, apply the retry rule, update `tool_hours.retries` and `tool_hours.first_call_ok`, and populate `tool_pairs` from consecutive calls.

The retry rule is on [First-call success](/metrics/first-call-success): the same tool called twice within one session inside 30 seconds, with a **different** `args_hash`.

Both writes group by **client** as well as tool, matching the key of `tool_hours` — so a client's first-call rate is those rows summed over tools rather than a second pass. The retry window itself still partitions by session and tool and not by client: a session belongs to one client, so adding it would change nothing and would suggest a call could be retried by a different model.

**02:00, not midnight.** The retry rule reads 30 seconds *past* the end of the day it is walking — a call at 23:59:50 can be answered at 00:00:05, and the pair has to be visible or the last call of every day is judged with its follow-up missing.

**Re-running a day is safe.** The pass assigns rather than adds, so backfilling a range is a loop over one day at a time.

## The other two jobs

| Time (UTC) |                                                |
| ---------- | ---------------------------------------------- |
| 02:00      | The nightly pass                               |
| **02:15**  | [Alert evaluation](/alerts/rules)              |
| 03:00      | Cleanup of old `calls` — **not yet scheduled** |

Alerts run fifteen minutes later, not beside the pass. `first_call_rate` is *written* by the pass, so a rule on it evaluated in the same minute would read a half-computed rate — firing on a figure that was never true, or missing one that was.

Cleanup must run after the pass, never before: deleting yesterday's calls first makes retries silently zero, forever, with no error anywhere.

## nightly\_as\_of

Every metric response carries it: the **last day the pass has covered**, or `null` when it has covered none of the range.

```json theme={null}
{ "range": { "from": "2026-07-16", "to": "2026-08-14" }, "nightly_as_of": "2026-08-13" }
```

Use it to label the three lagging figures. The dashboard shows them as *as of yesterday*.

## Today is excluded from the denominator, not just the numerator

This is the part worth understanding, because it explains a number that would otherwise look wrong.

`first_call_ok` is zero on days the pass has not walked. If today's calls were in the denominator, the headline metric would sag through the day and jump back at 02:00 — a number that moves on its own is worse than one that is a day old.

So the denominator is **calls on covered days only**. A range that is entirely today has no rate at all.

## Three states, not two

| Situation                              | Value    | Renders as                                   |
| -------------------------------------- | -------- | -------------------------------------------- |
| No calls in range                      | `0`      | `0%` — nothing happened is a complete answer |
| Calls exist, pass has covered them     | the rate | the rate                                     |
| Calls exist, pass has not reached them | `null`   | *not yet computed*                           |

That third case is the one place in MCPulse where "not yet" is honest rather than evasive. A zero there would claim every call failed.

In the per-day sparkline the same three states apply: a day with no calls plots zero and keeps the line continuous, and a day the pass has not reached is `null` so the line **breaks** rather than inventing a dip.

## First day

On day one, all three read as not yet computed. Insights that depend on first-call success do not fire. This is normal.

## Events exist so they can be deleted

The pass reads `calls`, the raw per-call rows. Only one other thing does — the [live call feed](/metrics/live-calls), which needs rows to point at. Every dashboard *figure* comes from `tool_hours`.

That separation is deliberate: calls pile into the millions, while an hour's counters stay tiny and add up correctly across any range. A cleanup of `calls` by your plan's retention is built and deliberately not switched on yet, and it must run after the nightly pass.

## Related

* [First-call success](/metrics/first-call-success)
* [Date ranges](/api/concepts/date-ranges)
