Konstas.Engineering Notes
Networking · Cybersecurity · Infrastructure
Networking

Network Jitter, Standard Deviation & Bandwidth Measurement

Articles · 2026 · Ioannis (John) Konstas

A technical reference for network engineers — covering RTT variance analysis, jitter classification, bandwidth testing, and annotated Python diagnostic tools.

Networking  ·  Python  ·  ICMP / ping  ·  Ioannis (John) Alexander Konstas

Jitter Standard Deviation RTT Analysis Bandwidth Testing ICMP Ping QoS VoIP Python
Core Concepts
§1

What Is Jitter?

Jitter is the variation in packet arrival times — specifically, how much individual Round Trip Times (RTTs) deviate from the average. A perfectly stable connection delivers every packet with the same delay. Real networks do not: queueing, routing changes, CPU load spikes, and link congestion all introduce fluctuation.

Jitter is measured in milliseconds. Low jitter means predictable delivery — critical for real-time traffic. High jitter means packets arrive unevenly, causing audio gaps, video stutters, and VoIP call degradation even when average latency looks acceptable.

Key insight: A connection with average RTT = 30ms and jitter = 40ms is worse for VoIP than one with average RTT = 80ms and jitter = 2ms. Jitter, not average latency, determines real-time quality.

What Is Standard Deviation in This Context?

Standard deviation (σ) measures how spread out RTT values are around the mean. In networking, it is closely related to jitter but uses Bessel's correction (divides by n−1 instead of n), making it an unbiased estimator for the population variance when working from a sample of pings.

What Is RTT?

Round Trip Time is the elapsed time from sending an ICMP echo request to receiving the echo reply. It includes propagation delay (speed of light in fiber), transmission delay (link speed), processing delay (router CPU), and queueing delay (congestion buffers). The ping utility measures RTT for each packet.

What Is Bandwidth?

Network bandwidth is the maximum data throughput — how many megabits per second can be transferred. Jitter and bandwidth are independent: a high-bandwidth link can have terrible jitter if the path traverses congested nodes. The bandwidth tool uses speedtest-cli to measure download and upload throughput against Speedtest.net infrastructure.

The Math — Jitter vs Standard Deviation
§2

The two scripts use two related but distinct formulas. Understanding the difference matters when interpreting output.

Jitter formula (population RMS deviation)
# RMS deviation — does NOT apply Bessel's correction
# Equivalent to: sqrt(E[X²] - E[X]²)  which equals population std dev

avg    = sum(times) / n
sumsq  = sum(t**2 for t in times)
jitter = sqrt(sumsq / n  -  avg**2)

# Mathematical identity:
# E[X²] - E[X]²  ≡  Var(X)  (population variance)
# sqrt of that   ≡  σ (population standard deviation)
# So: jitter here = population std dev (no Bessel correction)
Standard deviation formula (sample — Bessel's correction)
# Sample standard deviation — divides by (n-1) not n
# Bessel's correction removes bias when estimating from a sample

avg     = sum(times) / n
std_dev = sqrt(sum((t - avg)**2 for t in times) / (n - 1))

# When n is large (50+ pings), the difference between n and (n-1) is negligible
# Both values converge. The distinction matters most with small sample sizes.
Worked example — 5 ping RTTs
# RTT samples: 20ms, 22ms, 19ms, 35ms, 21ms
times   = [20, 22, 19, 35, 21]
n       = 5

avg     = (20+22+19+35+21) / 5   # = 23.4 ms
min_rtt = 19 ms
max_rtt = 35 ms

# Jitter (population std dev):
sumsq   = 20²+22²+19²+35²+21²   = 2971
jitter  = sqrt(2971/5 - 23.4²)   = sqrt(594.2 - 547.56) = sqrt(46.64) ≈ 6.83 ms

# Standard deviation (sample, Bessel corrected):
variance= ((20-23.4)²+(22-23.4)²+(19-23.4)²+(35-23.4)²+(21-23.4)²) / (5-1)
        = (11.56+1.96+19.36+134.56+5.76) / 4
        = 173.2 / 4 = 43.3
std_dev = sqrt(43.3) ≈ 6.58 ms

# Result: jitter = 6.83ms, std_dev = 6.58ms — close but not identical
# Classification: GOOD (6–20ms range)

Why Two Formulas?

MetricFormulaDivides byBest for
Jitter (in script)sqrt(E[X²] − E[X]²)nReal-time signal quality, RFC 3550 (RTP)
Std Dev (in script)sqrt(Σ(x−μ)²/(n−1))n−1Statistical estimation of population spread
RFC 3550 JitterRunning avg of |delay diff|16 (EWMA)RTP stream jitter in VoIP/video (different formula)
Jitter Classification Thresholds
§3
Jitter (ms)RatingImpactTypical cause
≤ 5 msExcellentImperceptible — ideal for all traffic typesDirect fiber, LAN, well-engineered WAN
6 – 20 msGoodVoIP acceptable with small jitter buffer; video smoothTypical broadband, low congestion
21 – 50 msFairVoIP degradation audible; video buffering possibleCongested paths, Wi-Fi interference, ISP issues
> 50 msPoorVoIP unusable; real-time traffic drops; SLA breachNetwork congestion, packet loss, poor routing

RTT Latency Reference

Average RTTAssessmentTypical use case
< 20 msExcellentLAN, co-located servers, same city CDN
20 – 50 msVery goodRegional WAN, metro fiber
50 – 100 msGoodCross-country broadband
100 – 150 msAcceptableIntercontinental; VoIP still workable
> 150 msDegradedSatellite, congested WAN; VoIP impacted
Network Topology & Delay Sources
§4
RTT DELAY SOURCES — PACKET JOURNEY HOST ping src Gateway NAT / router ISP POP edge router Internet transit/core TARGET DNS / host Processing OS stack NAT lookup ARP cache Queueing congestion Propagation speed of light Server CPU ICMP handler RTT = Processing + Transmission + Propagation + Queueing Processing: OS/NIC overhead, NAT table lookup, ICMP responder CPU → usually < 1ms Transmission: Time to push packet bits onto the wire = size / link_rate → < 1ms on GbE Propagation: Distance ÷ (2×10⁸ m/s in fiber) → ~5ms per 1000km Queueing: Packets waiting in congested router buffers → main source of JITTER Jitter source: Queueing delay fluctuates packet-to-packet → causes RTT variance Stable source: Propagation + transmission are constant → do NOT cause jitter

Figure: RTT components across a typical internet path — queueing delay is the primary jitter source

Tool Architecture
§5
ScriptMeasuresDependenciesKey output
ping_jitter_calculator.pyRTT jitter, std dev, min/max/avgstdlib only (os, subprocess, math, shutil)Jitter + classification, no bandwidth
ping_jitter_bandwidth_calculator.pyRTT jitter + download/upload/pingstdlib + speedtest-cliFull network health snapshot

User-Configurable Parameters

ParameterOptionsEffect
Target host8.8.8.8 (Google), 1.1.1.1 (CF), 9.9.9.9 (Quad9), 208.67.222.222 (OpenDNS), customDetermines path; test multiple to isolate ISP vs endpoint
Packet size64, 128, 512, 1024 bytes, customLarger packets stress transmission queues more; reveal size-dependent QoS shaping
Interval0.2s, 0.5s, 1.0s, 2.0s, customFast intervals (0.2s) stress test; slow intervals (2s) sample over longer period
Ping countDefault 50, any integer50+ gives statistically stable jitter; 200+ for SLA-grade baseline
Install speedtest-cli (bandwidth script only)
pip3 install speedtest-cli

# Verify
python3 -c "import speedtest; print('OK')"
Jitter Calculator — Annotated Code
§6

Jitter-only script — no external dependencies. Uses stdlib subprocess to run the system ping command, parses RTT values from stdout, and computes all statistics in pure Python math.

ping_jitter_calculator.py — complete annotated script
import os, subprocess, math, shutil

# ── ANSI color codes ──────────────────────────────────────────────
RED    = "\033[91m"
GREEN  = "\033[92m"
YELLOW = "\033[93m"
RESET  = "\033[0m"

def classify_jitter(jitter):
    # Thresholds: ≤5=Excellent  6-20=Good  21-50=Fair  >50=Poor
    if   jitter <= 5:  return f"{GREEN}Excellent{RESET}"
    elif jitter <= 20: return f"{YELLOW}Good{RESET}"
    elif jitter <= 50: return f"{YELLOW}Fair{RESET}"
    else:               return f"{RED}Poor{RESET}"

def get_selection(options, prompt):
    # Numbered menu — returns the value for the chosen key
    print(prompt)
    for k, v in options.items(): print(f"{k}. {v}")
    while True:
        try:
            sel = int(input(f"Choose (default {list(options.keys())[0]}): ") or list(options.keys())[0])
            return options.get(sel)
        except ValueError: print(f"{RED}Enter a number{RESET}")

def calculate_jitter():
    while True:
        try:
            os.system('clear')

            # ── Target selection ─────────────────────────────────────
            dns_options = {1:"8.8.8.8", 2:"1.1.1.1", 3:"9.9.9.9",
                           4:"208.67.222.222", 5:"custom"}
            host = get_selection(dns_options, "Select DNS target:")
            if host == "custom": host = input("Custom host: ").strip()

            # ── Packet size ──────────────────────────────────────────
            pkt_opts = {1:64, 2:128, 3:512, 4:1024, 5:"custom"}
            packet_size = get_selection(pkt_opts, "Packet size (bytes):")
            if packet_size == "custom": packet_size = int(input("Size: "))

            # ── Interval ─────────────────────────────────────────────
            int_opts = {1:0.2, 2:0.5, 3:1.0, 4:2.0, 5:"custom"}
            interval = get_selection(int_opts, "Ping interval (s):")
            if interval == "custom": interval = float(input("Interval: "))

            ping_count = int(input("\nNumber of pings (default 50): ") or 50)

            # ── Build platform-aware ping command ─────────────────────
            if os.name == "nt":   # Windows
                cmd = f"ping -n {ping_count} -l {packet_size} {host}"
            else:                   # Linux / macOS
                cmd = f"ping -c {ping_count} -i {interval} -s {packet_size} {host}"

            result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True)

            # ── Parse RTT values from ping stdout ─────────────────────
            times = []
            for line in result.stdout.splitlines():
                if "time=" in line:
                    try: times.append(float(line.split("time=")[1].split()[0]))
                    except: pass

            if len(times) < 2:
                print(f"{RED}Not enough data — increase ping count{RESET}"); break

            # ── Statistics ───────────────────────────────────────────
            count   = len(times)
            avg     = sum(times) / count
            sumsq   = sum(t**2 for t in times)
            jitter  = math.sqrt(sumsq / count - avg**2)      # population std dev
            min_t   = min(times)
            max_t   = max(times)
            std_dev = math.sqrt(sum((t - avg)**2 for t in times) / (count - 1))  # sample std dev
            rating  = classify_jitter(jitter)

            # ── Output ───────────────────────────────────────────────
            rows = [
                f"Target Host:         {host}",
                f"Ping Count:          {count}",
                f"Interval:            {interval:.2f} s",
                f"Packet Size:         {packet_size} bytes",
                f"Average RTT:         {avg:.2f} ms",
                f"Minimum RTT:         {min_t:.2f} ms",
                f"Maximum RTT:         {max_t:.2f} ms",
                f"Standard Deviation:  {std_dev:.2f} ms",
                f"Jitter:              {jitter:.2f} ms",
            ]
            w = min(max(len(r) for r in rows) + 4, shutil.get_terminal_size().columns - 4)
            border = "+" + "-" * w + "+"
            print(border)
            print("|" + "PING JITTER RESULTS".center(w) + "|")
            print(border)
            for r in rows: print(f"| {r.ljust(w-2)} |")
            print(border)
            print(f"| Jitter Classification: {rating.ljust(w-24)} |")
            print(border)

            if input("\nCalculate again? (y/n): ").strip().lower() != 'y': break

        except Exception as e:
            print(f"{RED}Error: {e}{RESET}"); break

if __name__ == "__main__":
    calculate_jitter()
Jitter + Bandwidth Calculator
§7

Extends the jitter calculator with a speedtest-cli call to measure download speed, upload speed, and ping against the nearest Speedtest.net server. Useful for correlating RTT jitter with actual throughput degradation.

ping_jitter_bandwidth_calculator.py — additional bandwidth section
import speedtest

def test_bandwidth():
    st = speedtest.Speedtest()
    st.get_best_server()           # select lowest-ping Speedtest server
    dl = st.download() / 1_000_000   # bps → Mbps
    ul = st.upload()   / 1_000_000   # bps → Mbps
    ping = st.results.ping             # ms to Speedtest server
    return dl, ul, ping

# Called after jitter stats are computed, adds to result_lines:
dl, ul, ping = test_bandwidth()
rows += [
    f"Download Speed:      {dl:.2f} Mbps",
    f"Upload Speed:        {ul:.2f} Mbps",
    f"Speedtest Ping:      {ping} ms",
]
Sample output — both scripts
+------------------------------------------+
|          PING JITTER RESULTS             |
+------------------------------------------+
| Target Host:         1.1.1.1             |
| Ping Count:          50                  |
| Interval:            0.20 s              |
| Packet Size:         64 bytes            |
| Average RTT:         12.34 ms            |
| Minimum RTT:         11.02 ms            |
| Maximum RTT:         19.87 ms            |
| Standard Deviation:  1.82 ms             |
| Jitter:              1.79 ms             |
| Download Speed:      412.50 Mbps         |
| Upload Speed:        98.20 Mbps          |
| Speedtest Ping:      14 ms               |
+------------------------------------------+
| Jitter Classification: Excellent         |
+------------------------------------------+
Interpreting Results
§8

High Jitter + Low Average RTT

Burst congestion upstream — the path is fast on average but occasionally backs up. Common on cable/DOCSIS during peak hours. Fix: test at different times; check ISP SLA; enable QoS on router.

High Jitter + High Average RTT

Sustained congestion or a problematic hop. Run traceroute to isolate which hop introduces the latency spike. May indicate oversubscribed ISP transit or a routing path change.

Low Jitter + High Average RTT

Distance-limited — propagation delay dominates. Consistent but slow. VoIP may still work if jitter is ≤5ms despite high RTT. No fix for speed of light; use CDN / closer endpoints if possible.

Std Dev Significantly Higher Than Jitter

Outlier pings (timeouts, retransmits) are pulling sample std dev up more than population jitter. Review max RTT — a single 500ms spike in 50 pings will inflate std dev but barely move jitter. Filter or re-run without the outlier condition to diagnose.

Low Bandwidth + Low Jitter

Link is clean but under-provisioned. Jitter target is met but throughput SLA is not. Traffic-shaping or QoS is not the issue — provisioned bandwidth is insufficient.

Comparing Targets

TargetWhat it testsGood for
8.8.8.8Path to Google's DNS anycast networkGeneral ISP path quality
1.1.1.1Path to Cloudflare anycastCDN path baseline
9.9.9.9Quad9 DNSSecurity-filtered path comparison
Gateway IPLocal CPE/router onlyIsolate local LAN vs ISP jitter
Custom hostApplication server or VoIP gatewayEnd-to-end SLA measurement
QoS & Real-World Impact
§9
Traffic typeJitter toleranceMax RTTNotes
VoIP (G.711)< 20ms< 150msITU-T G.114 recommendation; jitter buffer absorbs ≤30ms
Video conferencing< 30ms< 150msAdaptive bitrate helps but jitter causes pixelation
Online gaming< 15ms< 50msJitter causes rubber-banding; position prediction fails
HTTP/webUp to 100ms< 300msTCP retransmit handles it; perceived as slow page load
File transferTolerantAnyTCP flow control adapts; throughput may drop with high jitter
RTP streaming< 50ms< 200msRFC 3550 jitter estimator — different formula than this tool
Command Reference
§10

Manual Ping Diagnostics

Platform ping commands
# Linux/macOS — 50 pings, 64-byte, 0.2s interval
ping -c 50 -i 0.2 -s 64 1.1.1.1

# Linux — flood ping (root only) — maximum stress test
sudo ping -f -c 1000 -s 1024 1.1.1.1

# macOS — 100 pings with timestamps
ping -c 100 -D 8.8.8.8

# Windows — 50 pings with 512-byte packets
ping -n 50 -l 512 8.8.8.8

# Linux — compute jitter inline with awk after ping
ping -c 50 -q 1.1.1.1 | tail -1 | awk -F '/' '{print "avg="$5"ms  mdev(jitter)="$6"ms"}'

# mdev in Linux ping output = mean deviation ≈ jitter
Traceroute — isolate the jitter source
# Standard traceroute
traceroute 1.1.1.1

# With ICMP (not UDP) — works through more firewalls
traceroute -I 1.1.1.1

# MTR — live per-hop jitter + loss (best tool)
mtr --report --report-cycles 100 1.1.1.1

# MTR JSON output for scripting
mtr --json --report-cycles 50 1.1.1.1 | python3 -m json.tool

Compute Jitter Manually

Pure Python — paste RTT values, get stats
import math

times = [12.1, 11.8, 13.4, 25.0, 11.9, 12.2, 12.0, 11.7, 12.5, 12.1]

n       = len(times)
avg     = sum(times) / n
sumsq   = sum(t**2 for t in times)
jitter  = math.sqrt(sumsq / n - avg**2)                     # population
std_dev = math.sqrt(sum((t-avg)**2 for t in times) / (n-1)) # sample (Bessel)

print(f"n={n}  avg={avg:.2f}ms  min={min(times):.2f}ms  max={max(times):.2f}ms")
print(f"jitter={jitter:.2f}ms  std_dev={std_dev:.2f}ms")

← All articles

Research and direction by Ioannis (John) Konstas; compiled and finalized with AI assistance.

👁· views