Have you ever wondered if your internet speed actually matches your plan? Or why the Wi-Fi in the far corner of your house is so slow? To answer these questions, there's a professional tool called iperf3. In this guide, we'll set up an automated network monitoring system using it and send the results to VizIoT for clear, insightful visualization.
We will create a system that regularly measures network throughput to public iperf3 servers (also see this list on GitHub), and optionally, to your own private server.
iperf3 is a command-line utility for actively measuring the maximum achievable bandwidth on IP networks between two points: a client and a server.
Why is this useful? Unlike web-based speed tests, iperf3 measures the "raw" performance of your network channel, eliminating variables like browser performance or web server load. It's the "gold standard" for network engineers, allowing you to:
Our script will use three key modes to provide a complete picture of your network's health:
Upload: iperf3 -c <server>
Download: iperf3 -c <server> -R
-R (Reverse) flag flips the direction of the test.Bidirectional: iperf3 -c <server> --bidir
On the machine that will be running the tests, let's install everything we need.
Detailed documentation: https://nodejs.org
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install --lts
node -v
npm -v
sudo apt update
sudo apt install iperf3 -y
Now, let's create the script that will automatically run the tests and send the data.
Create a project directory:
mkdir viziot_iperf3
cd viziot_iperf3
Initialize a NodeJS project:
npm init -y
Enable ES Module support. Open the package.json file and add the line "type": "module",.
Install dependencies:
npm install viziot-mqtt-client-nodejs
Create the main script file:
nano main.js
Paste the following code into main.js. We'll start with an example test to a public server in Bulgaria.
import VizIoTMQTT from 'viziot-mqtt-client-nodejs';
import { spawn } from 'child_process';
// ===================================
// ========== CONFIGURATION ==========
// ===================================
// 1. Enter your VizIoT device key and password
const keyDevice = '________________';
const passDevice = '____________________';
// 2. Set the test interval (currently once per hour)
const intervalTime = 1000 * 60 * 60; // 1 hour in milliseconds
// 3. Configure the list of servers to test against.
// We'll start with one public server. You can add your own later.
const servers = [
{
// Example: Testing to a public server in Bulgaria
name: 'BG_Sofia',
command: '-c 37.19.203.1',
}
];
// ... the rest of the code remains unchanged ...
// ===================================
// ====== ADVANCED CONFIGURATION =====
// ===================================
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 30 * 1000;
const IPERF_TIMEOUT_MS = 60 * 1000;
// ... (paste the rest of the universal code from the previous answer here)
// ===================================
// ========= SCRIPT LOGIC ============
// ===================================
// ... (paste the rest of the universal code here)
const objMQTTClient = new VizIoTMQTT(keyDevice, passDevice);
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function runCommand(command, args, timeoutMs) {
return new Promise((resolve) => {
const child = spawn(command, args, { timeout: timeoutMs });
let stdout = '';
let stderr = '';
let timedOut = false;
child.stdout.on('data', (data) => (stdout += data.toString()));
child.stderr.on('data', (data) => (stderr += data.toString()));
child.on('error', (err) => (stderr += `\nFailed to start subprocess: ${err.message}`));
child.on('close', (code, signal) => {
if (signal === 'SIGTERM') {
timedOut = true;
stderr += '\nProcess was terminated by timeout.';
}
resolve({ stdout, stderr, code, timedOut });
});
});
}
async function runIperfTest(iperfPath, serverCommand, testMode) {
const testType = testMode.charAt(0).toUpperCase() + testMode.slice(1);
const iperfExe = iperfPath.replace(/"/g, '');
const args = serverCommand.trim().split(' ');
args.push('-J'); // JSON output
if (testMode === 'download') args.push('-R');
else if (testMode === 'bidir') args.push('--bidir');
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
console.log(`[INFO] Starting test: ${testType} for ${serverCommand.trim()}. Attempt ${attempt}/${MAX_RETRIES}...`);
const { stdout, stderr, timedOut } = await runCommand(iperfExe, args, IPERF_TIMEOUT_MS);
const errorResult = { upload: -1, download: -1 };
if (timedOut) {
console.error(`[ERROR] Test (${testType}) timed out after ${IPERF_TIMEOUT_MS / 1000} sec.`);
return errorResult;
}
if (!stdout) {
console.error(`[FATAL] iperf3 command returned no output (stdout). Stderr: ${stderr.trim()}`);
return errorResult;
}
try {
const result = JSON.parse(stdout);
if (result.error) {
if (result.error.includes('the server is busy')) {
console.warn(`[WARN] Server ${serverCommand.trim()} is busy. Attempt ${attempt} failed.`);
if (attempt < MAX_RETRIES) {
console.log(`[INFO] Waiting ${RETRY_DELAY_MS / 1000} seconds...`);
await delay(RETRY_DELAY_MS);
continue;
} else {
console.error(`[ERROR] Server still busy after ${MAX_RETRIES} attempts.`);
return errorResult;
}
} else {
console.error(`[ERROR] iperf3 returned an error:`, result.error);
return errorResult;
}
}
const finalResult = { upload: -1, download: -1 };
const summary = result.end;
if (summary.sum_sent) {
finalResult.upload = parseFloat((summary.sum_sent.bits_per_second / 1000000).toFixed(2));
}
if (summary.sum_received) {
finalResult.download = parseFloat((summary.sum_received.bits_per_second / 1000000).toFixed(2));
}
console.log(`[SUCCESS] Result (${testType}): Upload: ${finalResult.upload} Mbps, Download: ${finalResult.download} Mbps`);
return finalResult;
} catch (parseError) {
console.error(`[FATAL] Failed to parse JSON response from iperf3. Response:`, stdout);
return errorResult;
}
}
return { upload: -1, download: -1 };
}
async function getPacketAndSendToServer() {
const results = {};
const iperfPath = 'iperf3';
console.log(`[INFO] Using iperf3 at path: ${iperfPath}`);
for (const server of servers) {
console.log(`\n--- Starting test series for server: ${server.name} ---`);
const uploadResult = await runIperfTest(iperfPath, server.command, 'upload');
results[`${server.name}_upload`] = uploadResult.upload;
const downloadResult = await runIperfTest(iperfPath, server.command, 'download');
results[`${server.name}_download`] = downloadResult.download;
const bidirResult = await runIperfTest(iperfPath, server.command, 'bidir');
results[`${server.name}_bidir_upload`] = bidirResult.upload;
results[`${server.name}_bidir_download`] = bidirResult.download;
}
console.log('\n[SUCCESS] All tests completed. Final data packet:', results);
return results;
}
async function main() {
console.log('[INFO] Script starting. Connecting to VizIoT and running initial test...');
objMQTTClient.connect(() => console.log('[INFO] Successfully connected to VizIoT MQTT.'));
const initialPacket = await getPacketAndSendToServer();
objMQTTClient.sendDataToVizIoT(initialPacket, (err) => {
if (err) console.error(new Date(), 'ERROR sending initial data:', err);
else console.log(new Date(), 'Initial data sent successfully.');
});
setInterval(async () => {
console.log(`\n[INFO] Starting scheduled test (interval: ${intervalTime / 1000} seconds)...`);
const packet = await getPacketAndSendToServer();
objMQTTClient.sendDataToVizIoT(packet, (err) => {
if (err) console.error(new Date(), 'ERROR sending interval data:', err);
else console.log(new Date(), 'Interval data sent successfully.');
});
}, intervalTime);
}
main();
To ensure the script runs continuously in the background, we will use PM2, a popular process manager for NodeJS.
Install PM2 globally:
npm install pm2 -g
Start your script with PM2:
pm2 start main.js --name viziot_iperf3
The --name flag assigns a convenient name for managing the process.
Generate and configure the PM2 startup script for your system.
pm2 startup
This command will output another command (likely with sudo) that you need to copy and execute. This allows PM2 to launch on every system boot.
Save the current process list so PM2 remembers what to start:
pm2 save
Useful PM2 Commands:
pm2 list: Show a list of all running processes.pm2 logs viziot_iperf3: View your script's logs in real-time.pm2 restart viziot_iperf3: Restart the script.pm2 stop viziot_iperf3: Stop the script.Testing against public servers is good, but for maximum accuracy, it's best to test against a server you control (like a VPS or another computer on your local network).
On the server machine, install iperf3:
sudo apt update
sudo apt install iperf3 -y
Agree to run it as a service when prompted.
Open the port in your firewall:
sudo ufw allow 5201/tcp
Add your server to the script. Open the main.js file on your client machine and add a new object to the servers array.
Before:
const servers = [
{
name: 'BG_Sofia',
command: '-c 37.19.203.1',
}
];
After (example with a local server):
const servers = [
{
name: 'BG_Sofia',
command: '-c 37.19.203.1',
},
{
name: 'LOCAL_SERVER',
command: '-c 192.168.0.100', // Enter your server's IP here
}
];
After saving the changes, restart the script to apply the new server list:
pm2 restart viziot_iperf3
After the script runs for the first time, data will start flowing into your VizIoT device. All that's left is to create a beautiful dashboard.
BG_Sofia_upload, BG_Sofia_download, etc.).BG_Sofia_upload and BG_Sofia_download.