Measurements
What a node measures about itself and its peers, where to read it, and what the numbers mean. Everything here comes from traffic and work the node has anyway; it sends no probes of its own and nothing is measured by anyone else, but for the dashboard's map (LINKS, only while someone looks at it).
Four places to read:
GET /admin/stats: the node's figures as JSON, or as Prometheus text when asked for it (Accept: text/plainorapplication/openmetrics-text). Prometheus names arecontinuum_…; per-peer series carry apeerlabel (the node ID) and can be joined withcontinuum_peer_infofor the alias.GET /peers: one entry per known peer, keyed by its node ID, with the per-peer figures.- The log: incidents as they happen (a peer going dormant, a heal, a stalled catch-up),
at warn or info;
PUT /admin/logchanges the filter at runtime. GET /admin/dashboard(builds with thedashboardcargo feature): a page with the two above and a map of the swarm, live (dashboard.md).
All counters count since the process started; a restart resets them. Durations are
whole seconds in JSON unless named …Ms; Prometheus has seconds throughout.
About the node
| JSON | Prometheus | Meaning |
|---|---|---|
uptimeSecs |
uptime_seconds |
Since the process started. |
ready |
ready |
The node holds the swarm's document and serves writes (lifecycle.md). |
epoch |
epoch_generation |
The local epoch; the generation counts remakes since genesis. |
staleHelloReplies |
stale_hello_replies_total |
HELLO_REPLYs to a HELLO from an earlier run of this node, ignored. A few after every restart are normal (a transport queued them, Mycelium's above all); a steady stream means a peer keeps answering an old HELLO. |
The document
| JSON | Prometheus | Meaning |
|---|---|---|
document.heads |
document_heads |
Heads of the local document now. 1 means every change has been merged into one line of history; more means concurrent writes not yet followed by a merging write. |
document.maxHeads |
document_max_heads |
The most heads seen right after applying a sync message. A high value shows how concurrent the swarm's writing gets; it also bounds keepalive size (above 32 heads, keepalives carry a digest and 32 of them). |
document.savedBytes |
document_saved_bytes |
The document as Automerge saves it (compressed): roughly what a joining node downloads and what a document file rewrite writes. Measured in the background, only after a change, at least a minute apart and at most 1% of the time (a document that takes 2 s to save is measured every 200 s at most); an export (GET /admin/document) counts as a measurement. savedBytesSecsAgo and savedBytesTookMs say how fresh and how costly. |
document.lockWait.read, .write: count, sumUs, maxUs, buckets[] (leUs) |
document_lock_wait_seconds{mode} (histogram), document_lock_wait_max_seconds{mode} |
How long takers of the document's lock waited for it, since start, by mode. Readers (the API's reads, topology, stats, generating sync messages, serving changes and deltas, saves) wait only for a writer; writers (applying sync messages and fetched changes, writes, epoch switches) wait for everyone. Buckets from 10 µs to 10 s, the last unbounded; not cumulative in JSON. |
Interpretation. savedBytes growing without bound is the reason to compact
(remakes.md); savedBytesTookMs is a direct measure of what a switch, a rejoin and a
document file rewrite cost in CPU.
The node has one document, and nearly all work on it takes its lock, so lockWait says
whether more cores would help: waits of a few microseconds mean the lock is free when
asked for; write waits in the milliseconds (sumUs / count), growing with the number
of neighbours, mean applying one peer's sync messages queues behind the others'. A
high maxUs alone is a single long hold (a large sync message, a remake), not
contention.
Catching up
| JSON | Prometheus | Meaning |
|---|---|---|
catchUp.sources |
catch_up_sources_total |
Catch-up sources chosen. 1 is normal; more means the first source was given up (quiet, not ready, refused, or stalled) and another took over. |
catchUp.doneAfterSecs |
catch_up_duration_seconds |
From start until the initial sync was done; absent until then, and on a node that was ready at once (genesis, document file, recovery). |
catchUp.syncBytes |
catch_up_sync_bytes |
Sync payload received until then, from every peer: what joining cost. |
Compare syncBytes with savedBytes: much more received than the document is large
means the node got the document from several peers before the source rule took hold,
or caught up over many small messages.
Remakes
| JSON | Prometheus | Meaning |
|---|---|---|
remakes.undecided |
remakes_undecided |
Remakes of the local epoch not decided yet, expired ones included (GET /admin/remakes). |
remakes.committed |
remakes_committed |
Committed remakes of the local epoch: 1 for the moment between a commit and the switch. More than one is a double commit, on which no node switches by itself: alert on it. |
remakes.rejoining |
rejoining |
The node syncs a newer epoch from scratch (lifecycle.md). |
remakes.catchUpAttempts |
catch_up_attempts |
Attempts at catching up with a newer epoch; 0 when not catching up. A growing count means the node can't get the epoch its peers advertise. |
remakes.quarantined, remakes.oldestQuarantineSecs |
quarantined, oldest_quarantine_seconds |
Change sets set aside on a rejoin (GET /admin/quarantine). They live in memory only, so a restart loses them: alerting is how to learn of them. |
remakes.archivedEpochs |
archived_epochs |
Old epochs' documents kept in memory (archiveRetention). |
Sync and relay
| JSON | Prometheus | Meaning |
|---|---|---|
sync.lastFromNeighbourSecs |
last_sync_from_neighbour_seconds |
Since a sync message from any neighbour was applied. Growing without bound while others write: the node is cut off from the swarm's changes. |
sync.divergedPeers |
diverged_peers |
Peers whose heads differ from ours with no sync message in between. |
sync.largestReceivedBytes |
largest_sync_received_bytes |
The largest sync message received since start. Larger transfers come as deltas (blobs of 16 chunks or more, peers.md), so this stays below 16 × --blob-chunk from nodes of this version. |
relay.announceRounds |
announce_rounds_total |
Announcement rounds: our new heads to the neighbours after applying their changes (partial mesh only). |
relay.pulls |
pulls_total |
Pulls of changes we lacked from a neighbour that announced them. |
relay.pullsQueued |
pulls_queued_total |
Announcements of heads we lacked that waited for the pull in progress. Many compared with pulls: changes arrive from several sides at once. |
relay.blobRequestsDropped |
blob_requests_dropped_total |
Blob requests not answered because too many were being answered; the requester asks again or asks another peer. Also logged as a warning, at most once a minute with the count. |
Topology
| JSON | Prometheus | Meaning |
|---|---|---|
topology.k |
neighbours_target |
--neighbours. |
topology.view.members |
members |
Registered members, this node included. |
topology.view.reachable |
reachable_members |
Members other than this node not judged unreachable (three unanswered HELLOs, or dormant). |
topology.view.stable |
stable_members |
Members whose registry entry says stable. |
topology.view.neighbours |
neighbours |
Current neighbours: peers this node keeps alive and syncs with on its own. At least k (links chosen by either end count); well above k on a stable member that many others chose, bounded at about 8 × k. |
topology.view.stableNeighbours |
stable_neighbours |
Of those, stable members. |
topology.view.fullMesh |
full_mesh |
Every reachable member is a neighbour (at most k + 1 reachable members). |
view is absent until the node is ready and knows the member list; until then
everyone counts as a neighbour.
Interpretation. members − reachable − 1 is how many members are offline or
unreachable from here; if it is half or more of the electorate, no remake can commit.
neighbours far above k on one node means it is a hub (a stable member among few),
which is by design but costs it bandwidth.
Stability
| JSON | Prometheus | Meaning |
|---|---|---|
stability.tier |
stable |
What this node published about itself: stable once connected at least 95% of a full day, transient again below 80%. |
stability.connectedFraction |
connected_ratio |
Fraction of the observed time (up to 24 h) with at least one live peer. |
stability.observedSecs |
connectivity_observed_seconds |
How much time the fraction is based on; a day at most. |
A node that restarted keeps a stable tier only if a peer says it was away less than an
hour; otherwise it starts over with no history, and observedSecs says so.
Propagation
How long changes written elsewhere took to reach this node, from the writer's commit
stamp to the apply here, for every change in every applied sync message once caught up.
The apply is timed by the peers' clocks: this node's own offset from them
(clock.offsetMs, Clocks) is taken off.
A histogram with buckets at 100 ms, 250 ms, 500 ms, 1 s, 2.5 s, 5 s, 10 s, 30 s, 1 min,
5 min, 1 h, and the rest.
| JSON | Prometheus | Meaning |
|---|---|---|
propagation.count, .sumMs |
propagation_delay_seconds_count, _sum |
For the mean. |
propagation.buckets[] |
propagation_delay_seconds_bucket{le} |
Per bucket (not cumulative in JSON; cumulative in Prometheus, as histograms are). |
propagation.ahead |
propagation_clock_ahead_total |
Changes stamped later than their arrival (by the peers' clocks): the writer's clock is ahead. Counted in the first bucket. |
Interpretation. The delay includes the writer's clock skew (this node's own is taken
off), so changes from a writer with a badly set clock show nonsense here; ahead
growing is the sign, and that writer's own clock.off (trust.md, "What depends on
clocks"). Writers aren't known by node (Automerge's actors are per run), so their skew
can't be taken off too. With sane clocks: in a full mesh most
changes land in the first buckets; in a partial mesh each hop adds about a round trip
plus the 200 ms announcement delay. Mass in the minutes buckets means changes arrive by
keepalive-triggered pulls rather than relay, i.e. announcements are being lost.
Clocks
Each PONG says when its sender answered, by its clock, so every peer's clock is known
to within half a round trip (clock per peer, in Per peer). This node's own clock is
the odd one out when it differs from most of its active peers' by more than
--clock-skew-warn (2 s by default), for sure: beyond their bounds.
| JSON | Prometheus | Meaning |
|---|---|---|
clock.peers |
clock_peers_compared |
Active peers whose clocks were compared (a PONG from them lately). |
clock.offsetMs |
clock_offset_seconds |
How far this node's clock is ahead of its peers' (behind if negative): the median over them. Absent with none compared. |
clock.behind, clock.ahead |
— | Peers whose clocks are behind ours, and ahead, by more than --clock-skew-warn for sure. |
clock.off |
clock_off |
Whether this node's clock is off: more than half of them differ from it beyond the limit, the same way. |
clock.limitMs |
— | The limit (--clock-skew-warn). |
Interpretation. A peer or two ahead or behind are their clocks, which they warn
about themselves; off is ours. While it is, the log warns every 10 minutes, and says
when it agrees again, and /health says so in a second line (still 200). A clock set
anew (by NTP, a resumed machine, by hand) by more than the limit shows as a jump in the
log: this node's (a warning) when most peers' clocks moved alike, else the peers' whose
did. Two nodes alone can't tell whose clock is off: both warn. Clocks matter for alive
stamps, remake deadlines and the propagation figures (trust.md, "What depends on
clocks"); setting them is the operating system's job (NTP).
Traffic
Messages and payload bytes by direction (sent, received, refused) and kind
(hello, helloReply, sync, status, statusReply, blob, identity for WHO and
ME, and paths for PING and PONG). The layers below the node count WHO and ME, and the
paths layer's own PINGs and PONGs, under the address they went to or came from; a peer
(/peers) has those of its addresses too, where it is known to be at them. Payload bytes are what a transport's message carries (the wire
encoding): not the transport's framing (for Mycelium, its topic and the base64 of its
HTTP API). refused is what the transports wouldn't accept to send (no path to the
peer), not counted as sent.
| JSON | Prometheus | Scope |
|---|---|---|
traffic in /admin/stats |
messages_total{direction,kind}, payload_bytes_total{direction,kind} |
All peers, including ones no longer listed. |
traffic per entry in /peers |
peer_messages_total{peer,direction,kind}, peer_payload_bytes_total{peer,…} |
One peer. |
Interpretation. Keepalives (paths for PINGs and PONGs, every third one status + statusReply) should be about two per neighbour per
keepalive interval; much more means announcements (relay) or heals. sync bytes
dominate while catching up and on big writes; a steady sync stream on an idle swarm
means two peers disagree and keep healing. refused growing for one peer means no
transport has a path to it while we still consider it a neighbour.
Transports
How the transports find and reach peers, under transports in /admin/stats; each
part is absent (null) where it doesn't apply.
The identity layer (transport::identity, peers.md), under transports.identity:
| JSON | Prometheus | Meaning |
|---|---|---|
proven |
identity_proven_nodes{transport} |
Nodes proven at an address now, by transport; a node on several counts on each. |
relayed |
identity_relayed_nodes{transport} |
Of those, the nodes the transport reaches only through a relay (libp2p), by transport; in JSON absent where none is. Many means hole punching doesn't get through (libp2p.holePunches). |
hinted |
identity_hinted_nodes |
Nodes known only from the member list or HELLO_REPLYs, proven nowhere yet. |
proofsRejected |
identity_proofs_rejected_total |
WHO and ME that didn't hold: a bad signature, or made for other addresses. Normally 0; anything else is someone replaying or forging proofs. |
addressChanges |
identity_address_changes_total |
Nodes that proved themselves at another address on a transport than before: a restart with a new libp2p key or Mycelium key. Frequent ones mean a node doesn't keep its transport key (--libp2p-key-file, --mycelium-key-file). |
droppedUnproven |
identity_dropped_unproven_total |
Messages dropped because no node proved itself at their address (the address is asked WHO). Some after a restart are normal; a steady stream means something keeps sending from an address that never proves itself. |
refusedUnproven |
identity_refused_unproven_total |
Sends refused because the peer hadn't proven where it is yet; retried once it has. |
seedsUnanswered |
identity_seeds_unanswered_total |
Seeds at which no node answered WHO in time. |
The paths layer (transport::paths, peers.md), under transports.paths: the
paths to peers, measured by PINGs.
| JSON | Prometheus | Meaning |
|---|---|---|
paths[] (address, rttMs, down, connection) |
— | Every path measured: those of peers this node chose among or sent to in the last 5 minutes, and every path its keepalive PINGs went over (a peer's only path too). |
probesSent, probesAnswered, probesUnanswered |
paths_probes_total{result} |
PINGs, the keepalive's and every 30 s (or every keepalive interval, if that is longer) per path in use that no keepalive PING went over in that time (a busy pair needs no keepalives, so its paths would go unmeasured), and their PONGs. Unanswered ones (no PONG within 15 s) on one path mean it doesn't deliver; three in a row make it down. |
sendFailures |
paths_send_failures_total |
Sends that failed on a path; each is tried at once at the peer's next path. |
failovers |
paths_failovers_total |
Sends that went over another path after one failed. |
switches |
paths_switches_total |
Changes of the path in use for one faster by a quarter (and 5 ms). Frequent ones mean two paths about as fast, with changing delays. |
Per peer, in /peers (destination.paths), and as peer_path_rtt_seconds{peer,transport}
and peer_path_down{peer,transport}: each path to it, best first. A path's address is
where it goes now: for libp2p, the remote address of its connection, ending in the peer ID
(just the peer ID while there is none); a Mycelium public key; an onion service.
libp2p (--libp2p-enable), under transports.libp2p:
| JSON | Prometheus | Meaning |
|---|---|---|
reachability, publicAddress |
libp2p_reachability{status} |
What AutoNAT says: unknown, private (others can't dial this node: it needs a relay) or public, with the address it confirmed. |
connections.direct, .relayed |
libp2p_connections{kind} |
Open connections now, direct or through a relay. |
relaySlots.held |
libp2p_relay_slots |
Slots held on relays now, to be reachable through them. |
relaySlots.paused |
libp2p_relays_paused |
Relays not picked again for a while: 10 minutes after refusing a slot, 30 seconds after not granting it otherwise. |
relaySlots.granted, .lost, .notGranted |
libp2p_relay_slot_events_total{event} |
Slots granted (renewals not counted), lost after being granted (not when one moves: reserved again at once over the connection kept, as a duplicate connection to the relay closed), and asked for but never granted. notGranted growing: a relay is unreachable, or another peer answers at its address (a relay restarted without its libp2p key). |
holePunches.succeeded, .failed |
libp2p_hole_punches_total{result} |
Direct connections tried over a relayed one (DCUtR). A failure leaves the relay carrying the traffic. |
dialFailures |
libp2p_dial_failures_total{cause} |
Connections that couldn't be made: unreachable (refused, timed out, no route), wrongPeerId (another peer at the address: one restarted without its libp2p key, or a NATed peer's LAN address in a range ours uses too; that address isn't taken for the peer again), noAddresses, other. |
A relay that is no node (--relay-only) has /admin/stats too: its peerId, its
addresses (as libp2p: addresses, ready for _continuum/relays), uptimeSecs, and
transports.libp2p as above (Prometheus: relay_info{peer_id}, uptime_seconds and
the libp2p_… series).
Interpretation. A node that should be dialable but shows private has a port
closed or not forwarded. relayed connections that stay while holePunches.failed
grows means the relays carry the traffic for good: fine within their limits, but it
costs the relay. wrongPeerId and addressChanges together point at a node restarted
without its keys.
Incidents
Counted per peer, summed in /admin/stats over the peers currently listed (a
forgotten peer's are gone).
| JSON | Prometheus | Meaning |
|---|---|---|
incidents.replyTimeouts |
peer_reply_timeouts_total |
Our sync messages the peer didn't answer in time (10 s, 30 s with Tor enabled, doubling to 30 times that); resent. Lost messages, or a slow peer. |
incidents.heals |
peer_heals_total |
Re-handshakes because our heads and the peer's differed for resync_after with no sync between (peers.md, "Keepalive and self-healing"). Rare in a healthy swarm; frequent means sync state keeps getting out of step. |
incidents.dormancies |
peer_dormancies_total |
Times the peer went dormant (nothing received for dormant_after). |
incidents.dormantMs |
peer_dormant_seconds_total |
Time it spent dormant, up to its last return. |
Per peer
In /peers, and as peer_… series with a peer label:
| JSON | Prometheus | Meaning |
|---|---|---|
status, dormantForSecs |
peer_unknown, peer_dormant |
Unknown (nothing heard from it since this node started: known from the member list or another peer), active, or dormant, and for how long. |
lastSeenSecsAgo |
peer_last_seen_seconds |
Since any message from it. |
neighbour |
peer_neighbour |
Whether we initiate towards it. |
destination |
peer_destination_info{transport,proven,connection} |
Where messages to it go: the address (its transport), whether it proved itself there (else sends wait for its proof), and the connection there: libp2p's direct or relayed, Tor's direct (an onion circuit); none on Mycelium; while relayed after a failed hole punch, punching: punches failed in a row (failed) and when it's tried again (nextInSecs). |
destination.paths[].rttMs, .down, .connection |
peer_path_rtt_seconds{transport}, peer_path_down{transport} |
Every path to it, best first: its round-trip time (smoothed, from PINGs), whether it is down, and the transport's connection there (direct or relayed, where it has connections). The first is the one in use: the peer's round-trip time. |
lastSyncSecsAgo |
peer_last_sync_seconds |
Since a sync message from it was applied (or the epoch changed). |
divergedForSecs |
peer_diverged_seconds |
How long its heads have differed from ours with no sync message from it in between; absent while they match. Long values precede a healing re-handshake (peers.md). |
syncThroughput |
peer_sync_throughput_bytes_per_second |
Sync bytes per second over exchanges of at least 32 KiB, round trip taken off, smoothed. Absent before the first large exchange; comes with its age (secsAgo) since large exchanges are rare. This is the rate the peer delivers at in practice (its apply time included), not the path's capacity. |
clock.offsetMs, .withinMs |
peer_clock_offset_seconds, peer_clock_offset_within_seconds |
How far its clock is ahead of ours (behind if negative), give or take. Each PONG says when the peer answered, by its clock, which lies between our PING going and the PONG coming: within half the round trip of its middle, however unevenly the two ways took. Of its paths and the last 5 minutes, the narrowest bound is kept; a sample it contradicts (a clock set anew) replaces it at once. Absent before a PONG from it. |
epoch, sameEpoch, heads |
— | What it last advertised; sameEpoch: false means we don't sync with it. |
handshakeComplete, hasPendingDelivery |
— | Session state, for debugging. |
Disk
Per kind of the node's own files (state, document, backup, remake), since
start: reads, readBytes, writes (whole files), writtenBytes, appends,
appendedBytes, errors. Prometheus: disk_…_total{file}. The document file is the
only one written steadily: appendedBytes over time is the write rate it costs the
device; a rising writes count there means rewrites because the appended part keeps
outgrowing the full save (busy swarm, or a --document-rewrite set low).
The dashboard
In builds with the dashboard cargo feature, GET /admin/dashboard is a page showing
/admin/stats, /peers and a map of the swarm, live: dashboard.md, and how it works
underneath (events, LINKS) at its end. Its LINKS are the one thing a node sends only to
measure: the links every member reports, while someone looks.
What is not measured
- Nothing about other nodes but what they send us: no probes, no remote figures (but the dashboard's LINKS).
- Clock skew between nodes that aren't each other's peers: only peers' clocks are
compared (
clock, per peer). - Remake durations and outcomes: the remake reports (
GET /admin/remakes) hold them. - Mycelium's own routing, retransmissions and queueing: the daemon's API has them.
