
NetworkHealthChecker
v1.0.0
Table of contents
- Overview
- Versions
- Library files
- How it works
- NetworkHealthChecker class description
- Build and connect to your project
- Test application
Overview
The NetworkHealthChecker C++ library provides a way to monitor the quality of a UDP network link between two endpoints and to detect when it gets better, worse, or stays stable. It is intended for applications that stream real-time data over UDP (live video, audio, telemetry) and need a simple feedback signal for their adaptive logic - bitrate control, buffering, FEC strength, and so on. The same class works in two roles, selectable at initialization time:
- Server - actively sends probe packets to the remote side every 100 ms, processes the replies, and computes the network health metrics.
- Client - passively waits for probe packets and echoes them back without modification. The client does not compute anything; it only serves as a reflector for the server’s measurements. Internally the library measures packet loss (via a 256-slot ring buffer indexed by sequence number) and round-trip jitter (via the RFC 3550 single-clock form). Both metrics are smoothed with EWMA filters and combined into a single
floatin[-1, +1], where positive means “improving relative to the previous tick”, negative means “degrading”, and zero means “stable” (or “still in warm-up”). See Calculations for the math. Everything runs in its own thread spawned atinit..., so the calling application is free to pollgetNetworkStatus()from any other thread without locking. All clock measurements happen on the server side, so the two endpoints do not need to have synchronized clocks. The library depends only on the UdpSocket library (source code included, Apache 2.0 license).
Versions
Table 1 - Library versions.
| Version | Release date | What’s new |
|---|---|---|
| 1.0.0 | 06.05.2026 | First version. |
Library files
The library is supplied as source code only. The user is provided with a set of files in the form of a CMake project (repository). The repository structure is shown below:
CMakeLists.txt ------------------------ Main CMake file of the library.
3rdparty ------------------------------ Folder with third-party libraries.
CMakeLists.txt -------------------- CMake file to include third-party libraries.
UdpSocket ------------------------- Folder with UdpSocket library files.
src ----------------------------------- Folder with library source code.
CMakeLists.txt -------------------- CMake file of the library.
NetworkHealthChecker.cpp ---------- C++ implementation file.
NetworkHealthChecker.h ------------ Main library header file.
NetworkHealthCheckerVersion.h ----- Header file with the library version.
NetworkHealthCheckerVersion.h.in -- File for CMake to generate version header.
test ---------------------------------- Folder for the test application.
CMakeLists.txt -------------------- CMake file for the test application.
main.cpp -------------------------- Source C++ file of the test application.
How it works
Roles and data
The server sends packets - each containing a packet number [0, 255] in the first byte - to the client. The server is configured with the client’s IP address and the UDP port that the client listens on. The client receives a packet from the server and sends it back without any modification. The client does not need to be configured with the server’s address - it simply replies to whichever endpoint the incoming packet came from.
Calculations
The server thread runs on a fixed tick of 100 ms. On every tick it sends one probe packet, drains any replies that have arrived, updates the round-trip statistics, and re-evaluates the network health. All calculations live in the server thread, no locking is required for internal state, and only the final health value is exposed to the outside world via an std::atomic<float>.
Jitter
Jitter is computed as the smoothed variation of round-trip time between consecutive probes. The classical RFC 3550 inter-arrival jitter formula
D(i, i-1) = (R_i - R_{i-1}) - (S_i - S_{i-1})
is used in the single-clock form: both R (reply arrival time at the server) and S (probe send time at the server) live in the server’s clock, so the formula collapses to consecutive-RTT differences:
D = (R_i - R_{i-1}) - (S_i - S_{i-1})
= (R_i - S_i) - (R_{i-1} - S_{i-1}) // regroup
= RTT_i - RTT_{i-1} // since RTT = R - S
The two forms are mathematically identical, so the implementation tracks only the previous RTT sample (prevRttSampleUs) instead of separate R and S history.
The absolute value of D feeds an EWMA smoothing filter (α = 1 / 16) implemented with an integer accumulator scaled by 16 (jitterAccum = 16 * J), using fixed-point arithmetic:
jitterAccum += |D| - ((jitterAccum + 8) >> 4)
J(i) = jitterAccum >> 4 // in microseconds
The >> 4 operation is equivalent to dividing by 16. The (jitterAccum + 8) >> 4 rounding term ensures proper rounding of the fixed-point division.
Because both timestamps are server-local, no clock synchronisation between the server and the client is required; clock offset and skew between the two machines do not affect the measurement. The first received reply only seeds prevRttSampleUs - jitter starts being computed from the second reply.
Packet loss
Each outgoing probe is recorded in a ring buffer of 256 slots indexed by the low byte of the packet identifier. A slot stores the send time, a confirmed flag, and a used flag:
struct PacketSlot
{
/// Time the packet was sent (steady clock).
std::chrono::time_point<std::chrono::steady_clock> sendTimestamp;
/// True once the matching client reply has been processed.
bool confirmed;
/// True once the slot has been written at least once.
bool used;
};
When a reply arrives, the slot is looked up by the low byte of the echoed identifier and accepted only if slot.used == true and slot.confirmed == false. This rejects:
- duplicates - already-confirmed slots;
- alien packets - slots that were never used.
Once per tick, the ring buffer is scanned to compute the current loss ratio:
total- number of slots that areusedand older thanlossTimeoutUs;lost- subset oftotalwhereconfirmed == false;ratio = lost / total(or0iftotal == 0).
The age threshold lossTimeoutUs is adaptive and depends on a smoothed round-trip time. The smoothed RTT (SRTT) is maintained per RFC 6298 with α = 1 / 8:
SRTT = SRTT + (RTT_sample - SRTT) / 8
It is updated on every received reply, before the loss scan runs. The threshold itself is then:
lossTimeoutUs = max(300 ms, 5 * SRTT)
The 300 ms floor protects the metric during warm-up before SRTT is seeded and against unrealistically small RTT estimates on the loopback. The factor of five gives replies enough margin to arrive before the slot is treated as a loss; this avoids false positives on a loaded network with elevated RTT.
Slots that are too young are simply skipped - they are still in flight and contribute nothing to either side of the ratio.
Network health calculation
Health is reported as a single float in [-1, +1]:
- positive - the network is improving relative to the previous tick;
- negative - the network is degrading;
- zero - stable, or warm-up (the first 50 ticks always return
0).
It is built from two improvement deltas, both following the convention “previous − current -> positive when better”:
| Delta | Formula | Range |
|---|---|---|
lossDelta | prevLossRatio − currentLossRatio | [-1, 1] |
jitterDelta | (prevJitterUs − currentJitterUs) / max(prevJitterUs, 1 ms),clamped to [-1, 1] | [-1, 1] |
The 1 ms floor in the jitter normalisation prevents sub-millisecond noise from being amplified into spurious large deltas when the network is essentially perfect.
The two deltas are combined with weights tuned for a streaming workload:
rawStatus = 0.6 * lossDelta + 0.4 * jitterDelta
rawStatus = clamp(rawStatus, -1, +1)
Packet loss carries the larger weight because it directly translates into visible artifacts in the decoded video, whereas jitter only manifests as occasional stutter through the receiver’s jitter buffer.
Scaling and smoothing network health
To ensure values are sufficiently representative for video streaming and adaptive control, the combined metric is scaled by a factor of 1000 before being clamped to [-1, +1]:
scaledStatus = clamp(rawStatus * 1000, -1, +1)
To suppress jitter and make the signal more stable for adaptive control logic, the final status is smoothed using EWMA with α = 0.03 before being stored in m_networkStatus:
smoothedStatus += (scaledStatus - smoothedStatus) * 0.03
This means each new measurement has a weight of 3%, and the previous smoothed value has a weight of 97%. The smoothing significantly reduces high-frequency noise while still preserving the ability to detect rapid network condition changes.
Warm-up
During the first 50 ticks (~5 seconds) the result is forced to zero: this warm-up period gives SRTT, the loss ring, and the previous-jitter sample enough data to be meaningful. After warm-up, getNetworkStatus() returns the most recent smoothed status value computed by the server thread.
NetworkHealthChecker class description
NetworkHealthChecker class declaration
The NetworkHealthChecker class is declared in the NetworkHealthChecker.h file. Class declaration:
class NetworkHealthChecker
{
public:
/// Class constructor.
NetworkHealthChecker() = default;
/// Class destructor.
~NetworkHealthChecker();
/// Get string of the current class version.
static std::string getVersion();
/// Initialize the network health checker as a client.
bool initAsClient(uint16_t clientUdpPort);
/// Initialize the network health checker as a server.
bool initAsServer(uint16_t clientUdpPort,
std::string clientIp);
/// Stop the network health checker.
void stop();
/// Check if the network health checker is initialized.
bool isInit();
/// Get the current network status.
float getNetworkStatus();
};
getVersion method
The getVersion() method returns the version string of the NetworkHealthChecker class. Method declaration:
static std::string getVersion();
This method can be used without a NetworkHealthChecker class instance:
std::cout << "NetworkHealthChecker class version: " << NetworkHealthChecker::getVersion() << std::endl;
Console output:
NetworkHealthChecker class version: 1.0.0
initAsClient method
The initAsClient(…) method initializes the object as a client and starts the internal client thread that waits for probes and echoes them back. Method declaration:
bool initAsClient(uint16_t clientUdpPort);
| Parameter | Description |
|---|---|
| clientUdpPort | UDP port to bind the client socket to. |
Returns: true if the client was initialized successfully, false otherwise.
initAsServer method
The initAsServer(…) method initializes the object as a server and starts the internal server thread that periodically sends probes, processes replies, and computes the network health metrics. The server’s local UDP port is auto-selected: the method scans the range 10000-65535 and binds to the first free port. Method declaration:
bool initAsServer(uint16_t clientUdpPort, std::string clientIp);
| Parameter | Description |
|---|---|
| clientUdpPort | UDP port of the client to send packets to. |
| clientIp | IP address of the client to send packets to. |
Returns: true if the server was initialized successfully, false otherwise.
stop method
The stop() method stops the internal thread (server or client) and closes the UDP socket. After this call the object can be safely destroyed or re-initialized via initAsClient(...) or initAsServer(...). Method declaration:
void stop();
isInit method
The isInit() method returns whether the object has been initialized as a server or a client. Method declaration:
bool isInit();
Returns: true if the object has been initialized as a server or a client, false otherwise.
getNetworkStatus method
The getNetworkStatus() method returns the most recent network health value computed by the server thread. The method is thread-safe and can be polled at any time. On the client side it always returns 0, since the client does not compute health. Method declaration:
float getNetworkStatus();
Returns: a float in [-1, +1] indicating whether the network is degrading (value < 0), stable or in warm-up (value == 0), or improving (value > 0). See Network health calculation for the formula.
Build and connect to your project
Typical commands to build the NetworkHealthChecker library:
cd NetworkHealthChecker
mkdir build
cd build
cmake ..
make
If you want to connect the NetworkHealthChecker library to your CMake project as source code, you can follow these steps. For example, if your repository has the following structure:
CMakeLists.txt
src
CMakeLists.txt
yourLib.h
yourLib.cpp
Create a 3rdparty folder in your repository and copy the NetworkHealthChecker repository folder to the 3rdparty folder. The new structure of your repository:
CMakeLists.txt
src
CMakeLists.txt
yourLib.h
yourLib.cpp
3rdparty
NetworkHealthChecker
Create a CMakeLists.txt file in the 3rdparty folder. The CMakeLists.txt should contain:
cmake_minimum_required(VERSION 3.13)
################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)
################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)
################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
if (${PARENT}_SUBMODULE_NETWORKHEALTHCHECKER)
add_subdirectory(NetworkHealthChecker)
endif()
The 3rdparty/CMakeLists.txt file adds the NetworkHealthChecker folder to your project and excludes the test application (by default, the test application is excluded from compilation if NetworkHealthChecker is included as a sub-repository). Your repository’s new structure will be:
CMakeLists.txt
src
CMakeLists.txt
yourLib.h
yourLib.cpp
3rdparty
CMakeLists.txt
NetworkHealthChecker
Next, you need to include the 3rdparty folder in the main CMakeLists.txt file of your repository. Add the following line at the end of your main CMakeLists.txt:
add_subdirectory(3rdparty)
Next, you need to include the NetworkHealthChecker library in your src/CMakeLists.txt file:
target_link_libraries(${PROJECT_NAME} NetworkHealthChecker)
Done!
Test application
The test application (test/main.cpp) demonstrates how the NetworkHealthChecker library works.
#include <cstdio>
#include <iostream>
#include <string>
#include <thread>
#include "NetworkHealthChecker.h"
using namespace cr::clib;
using namespace std;
int main(int argc, char* argv[])
{
cout << "NetworkHealthChecker v" << NetworkHealthChecker::getVersion() << " test" << endl;
// Set type: server or client.
cout << "Select role: [s]erver / [c]lient: ";
char role = 0;
cin >> role;
// NetworkHealthChecker instance.
NetworkHealthChecker checker;
// Initialize as server.
bool isServer = false;
if (role == 's' || role == 'S')
{
// Get client IP and port from user input.
// Server will send UDP packets to this endpoint.
uint16_t clientPort = 0;
cout << "Client UDP port: ";
cin >> clientPort;
string clientIp;
cout << "Client IP: ";
cin >> clientIp;
// Init server with the client endpoint.
if (!checker.initAsServer(clientPort, clientIp))
{
cout << "Initialization failed" << endl;
return 1;
}
cout << "Server mode initialized" << endl;
isServer = true;
}
// Initialize as client.
else
{
// Get local UDP port to bind the client socket to.
// The client will listen for packets from the server on this port, and reply to them.
uint16_t port = 0;
cout << "Client UDP port: ";
cin >> port;
// Init client with the local port.
if (!checker.initAsClient(port))
{
cout << "Initialization failed" << endl;
return 1;
}
cout << "Client mode initialized" << endl;
}
// Main loop.
while (true)
{
if (!isServer)
cout << "Client is running. Waiting for packets from the server..." << endl;
else
cout << "Current network status: " << checker.getNetworkStatus() << endl;
// Wait.
this_thread::sleep_for(chrono::milliseconds(500));
}
return 0;
}
To run the application on Linux, execute the following commands. On Windows, just launch NetworkHealthCheckerTest.exe:
cd <application folder>
chmod +x NetworkHealthCheckerTest
./NetworkHealthCheckerTest
On startup the application prints its version and asks which mode to run in - server or client:
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient:
Type s to start in server mode, or c to start in client mode. The two modes are described below.
Client mode
After client mode is selected, the application asks for the UDP port to bind the client socket to:
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient: c
Client UDP port:
Once the port is entered, the client starts listening on that port and echoes back every probe received from the server:
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient: c
Client UDP port: 48981
Client mode initialized
Client is running. Waiting for packets from the server...
The client itself does not compute or print any network health value - that is done on the server side. Press Ctrl + C to stop the application.
Server mode
After server mode is selected, the application asks for two parameters in turn: the UDP port that the client is listening on, and the client’s IP address. The server’s own UDP port is auto-selected by initAsServer(...) - the first free port in the 10000-65535 range is used.
The UDP port that the client has bound to (where probes will be sent):
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient: s
Client UDP port:
The client’s IP address:
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient: s
Client UDP port: 48981
Client IP:
Once both parameters are entered, the application starts sending probes to the client and printing the current network health value every 500 ms. During the warm-up period (~5 seconds) the value is forced to zero:
NetworkHealthChecker v1.0.0 test
Select role: [s]erver / [c]lient: s
Client UDP port: 48981
Client IP: 127.0.0.1
Server mode initialized
Current network status: 0
Current network status: 0
Current network status: 0
After warm-up, the value reflects the current network health - close to zero on a stable network, positive when conditions improve, negative when they degrade:
...
Current network status: 0.000359184
Current network status: 0.00388679
Current network status: -0.00282198
Current network status: -0.00266071
Current network status: 0.00438525
Current network status: 0.00305634
Current network status: -0.00137471
Current network status: 0.000525926
Current network status: -0.00067563
...
Press Ctrl + C to stop the application.