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

# Monitoring

> Read a session's health: the quick check, the full report, and the live dashboard.

Two calls, for two purposes.

```python theme={null}
follower.metrics()      # one flat dict, for a log line
follower.stats()        # a list of subjects, for a collector
```

On the leader the second one is called `get_stats()`. Same shape, same
contract.

## metrics()

The quick check. Everything in it is a number you can print each second.

```python theme={null}
m = follower.metrics()
print(m["state"], m["tick_jitter_ms"], m["tick_overruns"])
```

| Key | Watch for |
| - | - |
| `state` | anything other than `active` while you are driving |
| `tick_jitter_ms` | climbing means the loop is not keeping its period |
| `tick_overruns` | a tick that ran long. Rising means your reads or writes are too slow |
| `stale_rejected` | commands arriving later than `max_staleness_ms` |
| `applied_seq` | should track the leader's `seq` |
| `unresolved_observations` | commands citing an observation the follower no longer has |

## stats()

The full report. A flat list, one entry per subject: the link, the control loop,
each joint, each camera, the recorder and the session as a whole. A leader
reports its own half, including video sync and how long a frame takes to reach
the screen.

Every entry is shaped like WebRTC's `getStats()`, so a collector you already run
can scrape it unchanged. Counters are cumulative and never reset, so take two
snapshots and subtract to get a rate. Gauges end in `Current`.

Print one to see what your own system reports:

```python theme={null}
import json
print(json.dumps(follower.stats(), indent=2))
```

That is the reliable way to find a field name. The report describes the machine
in front of you, including one entry per joint and per camera you actually have.

### What to look for

* **Is the link healthy.** Round trip time, packet loss, and whether the send
  buffer is growing. A buffer that climbs means the channel cannot keep up with
  what you are sending.
* **Is the loop keeping its period.** Tick jitter and overruns. Rising overruns
  mean your reads or writes are too slow for the rate you asked for.
* **Is the arm tracking.** Per joint, the error between where it was told to go
  and where it got to, alongside how often the speed cap fired. Both large
  together means the speed limit is what is holding the arm back.
* **Is it acting on fresh data.** How old an observation was by the time the
  robot acted on it. This is the number that matters most.
* **Is the correlation good.** How many commands arrived with a usable
  observation id. The rest become guessed rows at export time, see
  [Exporting datasets](/robotics/guides/exporting-datasets#correlation-quality).
* **Is the recording keeping up.** Queue depth against capacity, and how many
  rows were dropped.

### Why there is no end-to-end latency

Nothing here reports operator-to-motor time, and that is deliberate. It spans
two machines with no shared clock, so producing a number would mean subtracting
two wall clocks, which is not a latency.

What the SDK reports as fact is measured on a single machine, which is why
observation-to-applied is exact and anything crossing the network is an
estimate.

<Warning>
  Keep to the same rule if you build your own timing on top of this. Two wall
  clocks from different machines do not subtract into a latency.
</Warning>

<Warning>
  On a leader that only receives, most link measurements are absent or `None`.
  A round trip time exists only for a stream you send. Absent is not zero, so
  render it as unknown rather than as healthy.
</Warning>

## Watching the other end

Each side publishes its report to the other at `stats_hz`, default 1 Hz.

```python theme={null}
@leader.on_stats
def _(reports):
    print("robot reported", len(reports), "subjects")

print(leader.remote_stats_age_ms)      # None until the first report arrives
print(leader.robot_safe_state)         # the arm's own word for its state
```

`remote_stats` is `[]` when nothing has arrived, which is a real state and not
an error: the far side may have `stats_hz=0`. A climbing
`remote_stats_age_ms` is the other machine going quiet, which the counters
inside the report cannot show because they simply stop.

Set `stats_hz=0` to stop publishing. `stats()` and `get_stats()` still work.

## Dashboard

A live page, served in process off a follower or a leader.

```python theme={null}
from zeroruntime.teleops import attach

dash = attach(follower, port=8080)
print(dash.url)
follower.run(hz=50)
```

<ParamField path="host" type="str" default="127.0.0.1">
  `0.0.0.0` to reach a headless robot host from another machine on the LAN.
</ParamField>

<ParamField path="port" type="int" default="8080">
  `0` picks a free one, which you read back off `.url`.
</ParamField>

<ParamField path="hz" type="float" default="2.0">
  How often it samples stats.
</ParamField>

<ParamField path="video_fps" type="float" default="10.0">
  Camera encode rate for the grid. `0` serves stats with no pictures, for a
  host with no CPU to spare.
</ParamField>

For a program that should still run when the port is taken, use `serve()`,
which degrades to doing nothing instead of raising:

```python theme={null}
from zeroruntime.teleops.dashboard import serve

with serve(follower, args.dashboard):
    follower.run(hz=50)
```

A falsy port disables it, so you never branch on whether the flag was passed.

<Note>
  The ROS 2 bridge has no dashboard. It publishes stats on a ROS topic
  instead. See [Monitoring](/robotics/integrations/ros2/reference#monitoring).
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Troubleshooting" icon="wrench" href="/robotics/guides/troubleshooting">
    Symptoms, and which of these numbers to read first.
  </Card>

  <Card title="Follower" icon="robot" href="/robotics/api-reference/python/follower#monitoring">
    Where these calls live, and the rest of the class.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.