Skip to content

Metrics

irrld edited this page Jul 30, 2026 · 6 revisions

Counters for a session and for a listener, grouped the way options are: what every transport has lives in common, and each transport gets its own group.

void SampleSession(PeerSession& session) {
  SessionMetrics m = session.metrics();

  // any transport
  uint64_t sent = m.common.messages_sent;
  uint64_t refused = m.common.send_failures;  // queue was full

  // `transport` says which per-transport group carries real values
  if (m.transport == ConnectionType::ZDT) {
    uint32_t rtt_us = m.zdt.srtt_us;
    uint32_t window = m.zdt.cwnd;  // far below the cap means congestion control
    uint64_t retransmits = m.zdt.retransmits;
  } else {
    uint64_t writes = m.tcp.writes;
  }
}

void SampleServer(Server& server) {
  ServerMetrics s = server.metrics();
  uint64_t accepted = s.connections_accepted;
  uint64_t rejected = s.zdt.cookies_rejected;  // ZDT only
}

Groups that do not apply stay zeroed rather than holding garbage, so reading the wrong one is harmless — unlike a union, which would be undefined.

Pull, not push

metrics() takes a snapshot. The hot path only bumps plain members owned by the thread that already owns the object: no atomics, no locks, no per-packet callback. That is what makes them cheap enough to leave on.

The consequence is that you must sample on a timer, not per packet. Reading them in a hot loop costs more than the counting does. A second is a reasonable interval; anything under 100 ms is measuring your own sampling.

Values may be slightly stale, since the owning thread may be mid-update. For counters that does not matter. For the sampled gauges below it means an occasional reading that lags reality by a tick.

Build with -DZNET_ENABLE_METRICS=OFF to compile them out entirely, or set collect_metrics = false per session to stop populating them.

Common counters

Counter Meaning
messages_sent / messages_received Whole messages
message_bytes_sent / message_bytes_received After encode, before transport framing
wire_bytes_sent / wire_bytes_received Including transport framing
send_failures SendPacket refused, e.g. the queue was full
outbound_queued Current queue depth. Sampled, not accumulated

send_failures is the one to alert on. A nonzero and rising value means the application is producing faster than the link drains, and packets are being refused — silently, unless you are checking SendPacket's return value.

The gap between message_bytes_sent and wire_bytes_sent is the transport's overhead: framing, headers, retransmits.

TCP counters

Counter Meaning
writes send() calls that succeeded
reads recv() calls that returned data

ZDT counters

Counter Meaning
datagrams_sent / datagrams_received UDP datagrams, not messages
retransmits Reliable datagrams sent again
naks_sent / naks_received Gaps reported to the peer, and by the peer
duplicates_dropped Deduped by the receiver
inbound_dropped Inbox was full, arrivals discarded
reassemblies_dropped Incomplete, timed out, or over the cap

And these gauges, all sampled rather than accumulated:

Gauge Meaning
srtt_us Smoothed round-trip estimate
rtt_min_us Windowed minimum, the baseline congestion is judged against
rto_us Current retransmit timeout
cwnd Congestion window, in datagrams
in_flight Unacked reliable datagrams
mtu What the handshake settled on

Reading them together

Is the link the bottleneck, or am I? Compare cwnd against max_datagrams_in_flight. A window sitting far below the cap means the controller is holding it there. A window at the cap with in_flight also at the cap means you are limited by the setting, not the network.

Is it congestion or corruption? ZDT backs off on queueing delay, so read srtt_us against rtt_min_us: a smoothed estimate well above the minimum is a queue building somewhere on the path. Retransmits with srtt_us near rtt_min_us is loss without queueing — a lossy link rather than a full one.

Am I dropping on the floor? inbound_dropped means arrivals were discarded because the inbox filled, which means the session is not being serviced fast enough. Usually a handler doing too much work on the session's thread; see Threading Model.

Server counters

Counter Meaning
connections_accepted Total ever accepted
connections_active Currently held

ZDT-only, listener scope — these exist because ZDT rejects connections before a session exists, so they cannot live on one:

Counter Meaning
handshakes_started First contact from a new address
handshakes_rejected Version mismatch, server full, banned
cookies_rejected Failed the return-routability check
rate_limited Per-source handshake cap hit
datagrams_unroutable Online datagram from an unknown peer

cookies_rejected and rate_limited rising together is the signature of a spoofed-source flood being turned away, which is what those mechanisms are for. Rising datagrams_unroutable on its own more often means peers whose NAT mapping changed than an attack.

Clone this wiki locally