Monitoring Real Network Speed with iperf3 and VizIoT

Does your internet speed match what's advertised in your plan? Why does Wi-Fi in the far room work slower than near the router? To get accurate answers to these questions, ordinary speed test websites aren't enough. You need a professional tool — iperf3.

In this guide, we'll set up an automatic monitoring system that will regularly measure the "pure" bandwidth of your network and send the results to VizIoT for clear visualization.

End Result: Your Personal Network Monitoring Dashboard

Before we dive into the technical details, let's see what we'll get. You'll be able to create an interactive dashboard that displays in real-time:

  • Download and upload speeds to different points on the internet.
  • Performance history of your connection to identify "dips" at certain times of day.
  • Speed comparison to your ISP and to remote servers.

This dashboard will allow you not just to "measure speed," but to continuously monitor the quality of your network connection.


What is iperf3 and Why is it Better Than Regular Speed Tests?

iperf3 is a command-line utility for measuring maximum network bandwidth between two points (client and server).

Unlike websites, iperf3 measures the "pure" performance of your connection, excluding factors like browser performance, web server load, or the impact of ads on the page. It's the "gold standard" for network engineers, allowing you to:

  • Verify your ISP's honesty: Find out the real speed they're providing you.
  • Find "bottlenecks": Determine what exactly is slowing down your local network (weak router, bad cable).
  • Assess Wi-Fi performance: Compare speeds in different parts of your home.

How Will We Measure Speed?

Our script will use three key modes to get the complete picture:

  1. Upload: Your computer sends data to the server. This test shows how fast you can upload files to the internet.
  2. Download: The server sends data to you. This is what's usually called "internet speed."
  3. Bidirectional: Data is transmitted in both directions simultaneously. This is a stress test showing how the network handles complex loads (e.g., video call + file download).

How to Do It: Step-by-Step Instructions

Setting up the system consists of three simple steps: preparing the environment, creating the script, and setting up automatic execution.

What You'll Need

  • Client computer: Any device running Linux (Raspberry Pi, home server, VPS).
  • Required software:
    • iperf3: Testing utility.
    • NodeJS: Runtime environment for the script.
    • PM2: Process manager for autostart.

Step 1: Environment Setup

On the machine that will run the tests, install everything necessary.

Installing NodeJS via NVM (recommended)

Detailed documentation: https://nodejs.org

  1. Install NVM:
     curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
  2. Activate NVM (without re-logging):
     \. "$HOME/.nvm/nvm.sh"
  3. Install the latest version of Node.js:
     nvm install 24
  4. Verify installation:
     node -vnpm -v

Installing iperf3

sudo apt update
sudo apt install iperf3 -y

Step 2: Creating and Configuring the Script

  1. Create a directory for the project:
     mkdir viziot_iperf3 && cd viziot_iperf3
  2. Initialize the project and install dependencies:
     npm init -y
     npm install viziot-mqtt-client-nodejs
  3. Enable ES module support: Open the package.json file and add the line "type": "module",.
  4. Create the main script file:
     nano main.js
  5. Paste the following code. Don't forget to specify your VizIoT device key and password. As a test target, we'll use one of the public iperf3 servers.
import VizIoTMQTT from 'viziot-mqtt-client-nodejs'
import { spawn } from 'child_process'

// ===================================
// ========== SETTINGS ===============
// ===================================

// 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 for testing.
const servers = [
    {
        // Example: Testing to a public server in Bulgaria
        name: 'BG_Sofia',
        command: '-c 37.19.203.1',
    }
];

// --- The rest of the code is universal and requires no changes ---

const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 30 * 1000;
const IPERF_TIMEOUT_MS = 60 * 1000;

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 = '', stderr = '', 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 due to timeout.';
            }
            resolve({ stdout, stderr, code, timedOut });
        });
    });
}

async function runIperfTest(iperfPath, serverCommand, testMode) {
    const testType = testMode.charAt(0).toUpperCase() + testMode.slice(1);
    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] Running test: ${testType} for ${serverCommand.trim()}. Attempt ${attempt}/${MAX_RETRIES}...`);
        const { stdout, stderr, timedOut } = await runCommand(iperfPath, args, IPERF_TIMEOUT_MS);
        const errorResult = { upload: -1, download: -1 };
        if (timedOut) { console.error(`[ERROR] Test (${testType}) exceeded timeout.`); return errorResult; }
        if (!stdout) { console.error(`[CRIT] iperf3 command returned no result. 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 is busy. Attempt ${attempt} failed.`);
                    if (attempt < MAX_RETRIES) { await delay(RETRY_DELAY_MS); continue; }
                    else { console.error(`[ERROR] Server busy after ${MAX_RETRIES} attempts.`); return errorResult; }
                } else { console.error(`[ERROR] iperf3:`, 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(`[CRIT] Failed to parse JSON response from iperf3. Response:`, stdout); return errorResult; }
    }
    return { upload: -1, download: -1 };
}

async function getPacketAndSendToServer() {
    const results = {};
    for (const server of servers) {
        console.log(`\n--- Starting test series for server: ${server.name} ---`);
        const uploadResult = await runIperfTest('iperf3', server.command, 'upload');
        results[`${server.name}_upload`] = uploadResult.upload;
        const downloadResult = await runIperfTest('iperf3', server.command, 'download');
        results[`${server.name}_download`] = downloadResult.download;
        const bidirResult = await runIperfTest('iperf3', 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 started. Connecting to VizIoT and running first 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] Running 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();

Step 3: Automatic Startup with PM2

To keep the script running 24/7, we'll use the PM2 process manager.

  1. Install PM2 globally: npm install pm2 -g
  2. Start the script: pm2 start main.js --name viziot_iperf3
  3. Set up PM2 autostart: pm2 startup (and execute the command it suggests)
  4. Save the process list: pm2 save

Useful PM2 commands:

  • pm2 list: Show list of running processes.
  • pm2 logs viziot_iperf3: View logs.
  • pm2 restart viziot_iperf3: Restart.

What's Next? Expanding the Monitoring

Now that you have a working system, it's easy to improve it.

Adding Your Own Server (recommended)

For maximum accuracy, it's best to test to a server you control (e.g., VPS or computer on your local network).

  1. On the server machine:
     sudo apt update && sudo apt install iperf3 -y
     sudo ufw allow 5201/tcp # Open port in firewall

    (Agree to run iperf3 in service mode)

  2. On the client machine: open main.js and add your server to the list.
     const servers = [
         {
             name: 'BG_Sofia', // Public server
             command: '-c 37.19.203.1',
         },
         {
             name: 'LOCAL_SERVER', // Your server
             command: '-c 192.168.0.100', // Specify your server's IP
         }
     ];
  3. Restart the script: pm2 restart viziot_iperf3.

Now your dashboard will show speed graphs to your personal server.

Visualization in VizIoT

After launching, data will start flowing to your device. Log into VizIoT, create a dashboard, and add widgets.

  • "Chart" type: Perfect for tracking the dynamics of ..._upload and ..._download.
  • "Gauge" type: Can be used to display the latest test results.

Experiment to create a dashboard that will be most informative for you.