Skip to content

Metrics

irrld edited this page Aug 4, 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

  // `connection_type` says which per-transport group carries real values
  if (m.connection_type == 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. A zero in m.zdt on a TCP session means "not that transport", not "nothing happened", which is what m.connection_type is there to disambiguate.

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.

The same choice means reading them from a thread other than the session's owner is a data race, however harmless the result looks. Sample from inside a handler or a tick callback; see Threading Model.

Build with -DZNET_ENABLE_METRICS=OFF to compile them out entirely.

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
invalid_frames Inbound frames that failed to decode. See max_invalid_frames in Configuration Reference
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
tail_probes Resends fired because acks went silent, before any NAK or timeout could. A lost burst tail cannot be NAKed, so the probe breaks that stall
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.

All of the above is ZDT. A TCP session populates common and tcp only, and has no window, no RTT estimate and no retransmit counters of its own, because the kernel owns all three.

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
admission_rejected Dropped by the allow/deny lists or the attempt throttle. See Configuration Reference

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