Network Jitter, Standard Deviation & Bandwidth Measurement
A technical reference for network engineers — covering RTT variance analysis, jitter classification, bandwidth testing, and annotated Python diagnostic tools.
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.
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 two scripts use two related but distinct formulas. Understanding the difference matters when interpreting output.
# 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)
# 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.
# 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?
| Metric | Formula | Divides by | Best for |
|---|---|---|---|
| Jitter (in script) | sqrt(E[X²] − E[X]²) | n | Real-time signal quality, RFC 3550 (RTP) |
| Std Dev (in script) | sqrt(Σ(x−μ)²/(n−1)) | n−1 | Statistical estimation of population spread |
| RFC 3550 Jitter | Running avg of |delay diff| | 16 (EWMA) | RTP stream jitter in VoIP/video (different formula) |
| Jitter (ms) | Rating | Impact | Typical cause |
|---|---|---|---|
| ≤ 5 ms | Excellent | Imperceptible — ideal for all traffic types | Direct fiber, LAN, well-engineered WAN |
| 6 – 20 ms | Good | VoIP acceptable with small jitter buffer; video smooth | Typical broadband, low congestion |
| 21 – 50 ms | Fair | VoIP degradation audible; video buffering possible | Congested paths, Wi-Fi interference, ISP issues |
| > 50 ms | Poor | VoIP unusable; real-time traffic drops; SLA breach | Network congestion, packet loss, poor routing |
RTT Latency Reference
| Average RTT | Assessment | Typical use case |
|---|---|---|
| < 20 ms | Excellent | LAN, co-located servers, same city CDN |
| 20 – 50 ms | Very good | Regional WAN, metro fiber |
| 50 – 100 ms | Good | Cross-country broadband |
| 100 – 150 ms | Acceptable | Intercontinental; VoIP still workable |
| > 150 ms | Degraded | Satellite, congested WAN; VoIP impacted |
Figure: RTT components across a typical internet path — queueing delay is the primary jitter source
| Script | Measures | Dependencies | Key output |
|---|---|---|---|
ping_jitter_calculator.py | RTT jitter, std dev, min/max/avg | stdlib only (os, subprocess, math, shutil) | Jitter + classification, no bandwidth |
ping_jitter_bandwidth_calculator.py | RTT jitter + download/upload/ping | stdlib + speedtest-cli | Full network health snapshot |
User-Configurable Parameters
| Parameter | Options | Effect |
|---|---|---|
| Target host | 8.8.8.8 (Google), 1.1.1.1 (CF), 9.9.9.9 (Quad9), 208.67.222.222 (OpenDNS), custom | Determines path; test multiple to isolate ISP vs endpoint |
| Packet size | 64, 128, 512, 1024 bytes, custom | Larger packets stress transmission queues more; reveal size-dependent QoS shaping |
| Interval | 0.2s, 0.5s, 1.0s, 2.0s, custom | Fast intervals (0.2s) stress test; slow intervals (2s) sample over longer period |
| Ping count | Default 50, any integer | 50+ gives statistically stable jitter; 200+ for SLA-grade baseline |
pip3 install speedtest-cli
# Verify
python3 -c "import speedtest; print('OK')"
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.
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()
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.
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", ]
+------------------------------------------+ | 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 | +------------------------------------------+
High Jitter + Low Average RTT
High Jitter + High Average RTT
traceroute to isolate which hop introduces the latency spike. May indicate oversubscribed ISP transit or a routing path change.Low Jitter + High Average RTT
Std Dev Significantly Higher Than Jitter
Low Bandwidth + Low Jitter
Comparing Targets
| Target | What it tests | Good for |
|---|---|---|
8.8.8.8 | Path to Google's DNS anycast network | General ISP path quality |
1.1.1.1 | Path to Cloudflare anycast | CDN path baseline |
9.9.9.9 | Quad9 DNS | Security-filtered path comparison |
Gateway IP | Local CPE/router only | Isolate local LAN vs ISP jitter |
| Custom host | Application server or VoIP gateway | End-to-end SLA measurement |
| Traffic type | Jitter tolerance | Max RTT | Notes |
|---|---|---|---|
| VoIP (G.711) | < 20ms | < 150ms | ITU-T G.114 recommendation; jitter buffer absorbs ≤30ms |
| Video conferencing | < 30ms | < 150ms | Adaptive bitrate helps but jitter causes pixelation |
| Online gaming | < 15ms | < 50ms | Jitter causes rubber-banding; position prediction fails |
| HTTP/web | Up to 100ms | < 300ms | TCP retransmit handles it; perceived as slow page load |
| File transfer | Tolerant | Any | TCP flow control adapts; throughput may drop with high jitter |
| RTP streaming | < 50ms | < 200ms | RFC 3550 jitter estimator — different formula than this tool |
Manual Ping Diagnostics
# 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
# 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
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")