Real-Time Traceroute Visualizer
Stream live traceroute hops into an interactive network graph with Flask and SSE.
Introduction
30 Second Summary
Every time you load a website, your data hops through a chain of routers you never see. Each hop adds latency, and any one of them can silently drop your packets into a black hole.
In this project, you will build a real-time traceroute dashboard that makes that invisible path visible. You will type a hostname, hit Trace, and watch an interactive network graph build itself live as each hop is discovered.
What You'll Build
You type a target into a web form, click Trace, and watch nodes appear one-by-one in an interactive graph. Colors shift from green to red based on latency, timeout hops glow grey, and when the trace finishes you have a full visual map of the route your packets took.
By the end of this project, you'll have:
- A running local web app where you trace any hostname or IP and watch each network hop appear live in the browser as a color-coded node in an interactive graph.
- A Python backend that streams structured data (hop number, IP, hostname, latency) to the browser in real time using Server-Sent Events, proving you can bridge a system command and a live frontend.
- An interactive Cytoscape.js graph you can click, pan, and zoom to explore the exact route your packets took across the internet.
- Secret Mission: Add a live latency bar chart below the graph that updates as hops arrive, showing RTT comparison across all hops at a glance.
Are there any prerequisites?
You should have basic Python knowledge (writing functions, using imports). Familiarity with TTL and ICMP concepts is helpful but not required. The project explains traceroute's mechanics as you build.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters for your networking knowledge.
Set Up Your Project and Install Flask
Your traceroute visualizer needs a web server to bridge the system traceroute command and your browser. You need something that can spawn a process, read its output line by line, and stream those lines to a connected client.
Flask is the simplest Python web framework for this job. It can serve HTML, handle requests, and stream data with zero heavy setup. In this step, you will install Flask and get a minimal server running.
In this step, get ready to:
- Create a project folder with a Python virtual environment.
- Install Flask and verify the installation.
- Run a minimal Flask app and see Hello World in your browser.
Create your project folder and virtual environment
Every Python project lives in its own folder with its own isolated set of packages. You will create that folder now and set up a virtual environment inside it.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir traceroute-visualizer
cd traceroute-visualizer
- Create and activate a Python virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python, so different projects can use different package versions without conflicting.
Don't see (venv) in your prompt?
Make sure you ran both commands. The first command (python3 -m venv venv) creates the environment. The second command (source venv/bin/activate) activates it.
If you closed and reopened the terminal, run cd ~/Desktop/traceroute-visualizer and then source venv/bin/activate again.
help me activate my Python virtual environment
Install Flask and verify
- Install Flask by running this command:
pip install flask==3.1.3
You will see pip downloading and installing Flask along with its dependencies. When it finishes, you will see a success message.
Why Flask?
Flask is a lightweight Python web framework. For this project, its key feature is streaming responses. Flask can return data to the browser line by line as it becomes available, instead of waiting for everything to finish first.
That streaming ability is exactly what you need to push traceroute output to the browser in real time.
Before you run the next command, what do you think it will print?
- Verify your Flask installation by running:
python3 -m flask --version
You should see output showing your Python version, Flask 3.1.3, and a Werkzeug version. This confirms Flask is installed correctly inside your virtual environment.
Seeing a different Flask version or command not found?
Make sure (venv) is showing in your prompt. If it is not, you installed Flask into your system Python instead of the virtual environment. Run source venv/bin/activate and then pip install flask==3.1.3 again.
help me verify my Flask installation
Create your Flask app and run it
Now you will create the minimal Flask application that serves a page. This proves your setup works end to end, from Python code to a response in your browser.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop and select the traceroute-visualizer folder, then click Open.
- Create a new file called app.py by clicking the new file icon in the Explorer sidebar.
- Paste the following code into app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
Flask(__name__) creates your web application instance. The @app.route('/') decorator tells Flask that when someone visits the root URL, it should call the index() function.
app.run(debug=True) starts the development server. Debug mode automatically restarts the server when you change your code, so you will not need to stop and restart manually during development.
- Save app.py with Cmd+S.
Before you run the server, what do you expect to see when you visit the URL in your browser?
- Switch back to your terminal and start the Flask server by running:
python3 app.py
What should I see in the terminal?
Flask will print a few lines including the URL where your app is running. Look for a line that says Running on http://127.0.0.1:5000. The server now takes over this terminal window until you stop it.
- Open your web browser and visit http://127.0.0.1:5000.
You should see the text "Hello World" displayed in the browser. Your Flask server is working.
Not seeing Hello World?
Check that the terminal is still showing the server running with no error messages. If you see a traceback, check app.py for typos. The @app.route('/') line must have a forward slash inside the quotes.
If the page says the site cannot be reached, make sure you typed the URL exactly as http://127.0.0.1:5000 with no extra characters.
help me get my Flask server running
- Return to your terminal and press Ctrl+C to stop the Flask server.
Now create the HTML template file that you will build on in the next step.
- Create the templates directory by running:
mkdir templates
- Switch back to VS Code. In the Explorer sidebar, you should see the new templates folder.
- Right-click the templates folder and select New File.
- Name the file index.html and press Enter.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
Why create this template now?
Flask uses a templates folder by convention to store HTML files. You already imported render_template in app.py. In the next step, you will switch the route to serve this HTML file instead of a plain string.
- Save index.html with Cmd+S.
- Confirm your project structure by running this in your terminal:
ls
You should see app.py, templates, and venv listed in the output.
- Verify the template file exists inside the folder by running:
ls templates
You should see index.html listed.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
if __name__ == '__main__':
app.run(debug=True)
And here is templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
Your environment is ready. Next up, you will run the traceroute command from Python and stream its raw output to the browser in real time.
Run Traceroute and Display Raw Output
You have a Flask app that says "Hello World." That proves Python and Flask work, but it does nothing with networking yet.
Before you can visualize anything, you need to prove you can execute traceroute from Python and get its output into the browser. This step produces the "ugly but functional" version: raw text dumped to the page, lines arriving one by one as each network hop responds.
In this step, get ready to:
- Build an HTML form where you enter a hostname or IP address.
- Create a streaming endpoint that runs traceroute and pushes each line to the browser in real time.
- Verify raw traceroute output appears live in the browser.
Build the HTML form and update the index route
Your current app.py returns a plain string. You need it to serve an actual HTML page with a form the user can type into.
- Replace the entire contents of app.py with this code:
import subprocess
from flask import Flask, Response, render_template, request, stream_with_context
app = Flask(__name__)
@app.route('/')
def index():
# Serve the frontend HTML page
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
- The imports bring in everything you will need: subprocess for running traceroute, and Flask utilities for streaming responses.
- The index() route now uses render_template('index.html') to serve the HTML file from your templates/ folder.
- Replace the entire contents of templates/index.html with this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Traceroute Visualizer</h1>
<input type="text" id="target" placeholder="Enter hostname or IP">
<button id="trace-btn">Trace</button>
<pre id="output"></pre>
<script>
// Start trace when button is clicked
document.getElementById('trace-btn').addEventListener('click', function() {
const target = document.getElementById('target').value.trim();
if (!target) return;
// Clear previous output
document.getElementById('output').textContent = '';
// Open SSE connection to the /trace endpoint
const es = new EventSource('/trace?target=' + encodeURIComponent(target));
es.onmessage = function(e) {
document.getElementById('output').textContent += e.data;
};
es.onerror = function() { es.close(); };
});
</script>
</body>
</html>
What does this code do?
- The <input> field lets you type a hostname or IP. The <button> triggers the trace.
- The <pre> element displays raw output with preserved whitespace.
- The script uses the browser's EventSource API to open a Server-Sent Events connection to /trace. Each event that arrives gets appended to the <pre> block.
✔️ Awesome, I've got everything!
Great. Make sure you have saved templates/index.html.
ⓧ I'd like to double check the full code
Here is the full templates/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Traceroute Visualizer</h1>
<input type="text" id="target" placeholder="Enter hostname or IP">
<button id="trace-btn">Trace</button>
<pre id="output"></pre>
<script>
document.getElementById('trace-btn').addEventListener('click', function() {
const target = document.getElementById('target').value.trim();
if (!target) return;
document.getElementById('output').textContent = '';
const es = new EventSource('/trace?target=' + encodeURIComponent(target));
es.onmessage = function(e) {
document.getElementById('output').textContent += e.data;
};
es.onerror = function() { es.close(); };
});
</script>
</body>
</html>
- Save both app.py and templates/index.html.
- In your terminal, stop the running server with Ctrl+C, then restart it by running:
python3 app.py
- Refresh http://127.0.0.1:5000 in your browser. You should see the heading "Traceroute Visualizer", a text input field, and a Trace button.
Still seeing Hello World?
- Make sure you saved app.py after replacing its contents. The index() function must return render_template('index.html'), not a string.
- Confirm your HTML file is at templates/index.html (inside the templates folder, not in the project root).
- Try a hard refresh in your browser (Cmd+Shift+R on macOS) to clear the cache.
My form isn't appearing after restarting the Flask server. Help me debug.
Add the traceroute streaming endpoint
The form is ready, but clicking Trace does nothing yet because the /trace endpoint does not exist. You need a function that spawns traceroute as a subprocess and streams its output line by line.
- In app.py, add this function between the app = Flask(__name__) line and the @app.route('/') decorator:
def run_traceroute_raw(target):
# Spawn traceroute as a subprocess with line-buffered output
proc = subprocess.Popen(
['traceroute', '-m', '20', '-q', '1', target],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# Read lines one at a time and yield each as an SSE event
while True:
line = proc.stdout.readline()
if line == '' and proc.poll() is not None:
break
if line.strip():
yield f'data: {line}\n\n'
proc.wait()
What does this code do?
- subprocess.Popen starts the traceroute command as a child process. The flags -m 20 (max 20 hops) and -q 1 (one probe per hop instead of the default three) keep the trace fast.
- bufsize=1 enables line buffering, so each hop line arrives immediately instead of being held in a buffer.
- The while True loop reads one line at a time. When readline() returns an empty string and the process has exited, the loop ends.
- Each non-empty line is yielded in SSE format: data: <line>\n\n. This is the format browsers expect from a Server-Sent Events stream.
- Below the index() function (and above if __name__ == '__main__':), add this new route:
@app.route('/trace')
def trace():
# Get the target hostname/IP from query params
target = request.args.get('target', '').strip()
if not target:
return 'Missing target parameter', 400
# Stream the traceroute output as Server-Sent Events
return Response(
stream_with_context(run_traceroute_raw(target)),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
)
What does this code do?
- request.args.get('target') reads the hostname from the URL query string (e.g. /trace?target=google.com).
- stream_with_context keeps Flask's request context alive while the generator yields data. Without it, the request would close before traceroute finishes.
- The response mimetype text/event-stream tells the browser this is an SSE stream. The Cache-Control header prevents caching stale results.
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py.
ⓧ I'd like to double check the full code
Your app.py should have these sections in order. Scroll through and confirm each is present:
- The import subprocess and from flask import ... lines at the top.
- app = Flask(__name__) followed by the run_traceroute_raw() function.
- The @app.route('/') route returning render_template('index.html').
- The @app.route('/trace') route returning a streaming Response.
- The if __name__ == '__main__': block at the bottom.
- Save app.py.
Test the raw stream
Both files are ready. Time to see if traceroute output actually streams into your browser live.
- Stop the running server with Ctrl+C, then restart it by running:
python3 app.py
- Refresh http://127.0.0.1:5000 in your browser.
- Type google.com in the input field.
Before you click Trace: do you think the output will appear all at once, or line by line with a pause between each?
- Click Trace.
You should see raw traceroute text appearing in the browser line by line, with each hop taking a second or two to resolve. The first line shows something like traceroute to google.com (...), 20 hops max, 52 byte packets. Then each hop line arrives one at a time.
Why do lines appear one by one?
Each hop forces traceroute to wait for a response (or a timeout). Because you used bufsize=1 and read with readline(), each line is pushed to the browser the instant it arrives from the subprocess. This is the real-time streaming pattern you will build on.
Some hops may show * * * instead of a time. Those are routers that do not respond to probes. You will handle these in the next step.
Nothing appears after clicking Trace?
- Check that you restarted the server after saving app.py. Flask does not automatically pick up changes to routes unless debug mode catches it. Stop with Ctrl+C and re-run python3 app.py.
- Open your browser's DevTools console (Cmd+Option+J on macOS). If you see a 404 error for /trace, the route was not added correctly. Confirm @app.route('/trace') appears above the def trace(): function.
- If you see a permission error in the terminal mentioning traceroute, try running which traceroute to confirm the command exists on your system. On macOS it should be at /usr/sbin/traceroute.
My traceroute stream is not working. Help me debug.
You have raw traceroute output streaming live to the browser. It works, but it is just a wall of text. Next up, you will parse this raw output into structured data so you can visualize each hop as a node on a graph.
Parse Traceroute Output into Structured Hop Data
Your raw traceroute output is streaming to the browser. Every hop line arrives in real time. Now comes the harder question: how do you turn those messy text lines into data your future graph can actually use?
In this step, you will write a regex parser that extracts the hop number, IP address, hostname, and round-trip time from each traceroute line. You will also switch your SSE endpoint to emit structured JSON objects instead of raw text.
In this step, get ready to:
- Write a regex parser that converts raw traceroute lines into structured dictionaries.
- Integrate the parser into your SSE stream so the browser receives JSON hop objects.
- Update the frontend to display parsed hops in a structured table.
Write the regex parser
Each line of traceroute output follows a predictable format. A typical hop line looks like 1 router.local (192.168.1.1) 2.345 ms. Timeout hops show only asterisks instead of an IP and time.
Your parser needs to handle both cases and extract the meaningful fields into a Python dictionary.
- In app.py, find the import line at the top:
import subprocess
- Replace it with these three imports:
import json
import re
import subprocess
What are these imports for?
json serializes Python dictionaries into JSON strings that the browser can parse. re is Python's regular expression module for pattern-matching the traceroute output.
- Below the app = Flask(__name__) line, add the parse_hop() function by copying this code:
def parse_hop(line):
"""Parse a single traceroute output line into a structured dict."""
# Match the hop number at the start of the line
hop_match = re.match(r'\s*(\d+)\s+(.+)', line)
if not hop_match:
return None
hop_num = int(hop_match.group(1))
rest = hop_match.group(2)
# Handle timeout lines (only asterisks)
if re.match(r'^[\s\*]+$', rest):
return {'hop': hop_num, 'ip': None, 'host': None, 'rtt': None}
# Extract hostname and IP address
host_ip_match = re.search(r'(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)', rest)
host = None
ip = None
if host_ip_match:
host = host_ip_match.group(1)
ip = host_ip_match.group(2)
else:
# Sometimes only IP is shown without hostname
ip_only_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', rest)
if ip_only_match:
ip = ip_only_match.group(1)
host = ip
# Extract RTT values and average them
rtt_values = re.findall(r'([\d.]+)\s+ms', rest)
rtt = None
if rtt_values:
rtt = round(sum(float(r) for r in rtt_values) / len(rtt_values), 2)
return {'hop': hop_num, 'ip': ip, 'host': host, 'rtt': rtt}
What does this code do?
- The first regex extracts the hop number (the leading integer) and captures everything after it.
- If the rest of the line is only asterisks and spaces, it is a timeout hop. The function returns None values for IP, host, and RTT.
- For normal hops, it looks for the hostname followed by the IP in parentheses. If only an IP appears, it uses that for both fields.
- Finally, it finds all RTT values (floats followed by ms) and averages them into a single number.
- Save app.py.
- Test the parser by running this command in your terminal:
python3 -c "from app import parse_hop; print(parse_hop(' 1 router.local (192.168.1.1) 2.345 ms'))"
You should see a dictionary printed: {'hop': 1, 'ip': '192.168.1.1', 'host': 'router.local', 'rtt': 2.35}.
Seeing a ModuleNotFoundError?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate first.
Also confirm you are running the command from inside the traceroute-visualizer folder where app.py lives.
help me debug a ModuleNotFoundError when importing parse_hop from app.py
Integrate the parser into the SSE stream
The parser works on individual lines. Now you need to wire it into your traceroute generator so the browser receives JSON objects instead of raw text.
- In app.py, find the entire run_traceroute_raw function (everything from def run_traceroute_raw(target): down to proc.wait()).
- Delete it entirely and replace it with this new run_traceroute function:
def run_traceroute(target):
"""Generator that runs traceroute and yields parsed hop events."""
proc = subprocess.Popen(
['traceroute', '-m', '20', '-q', '1', target],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
while True:
line = proc.stdout.readline()
if line == '' and proc.poll() is not None:
break
if line.strip():
# Parse the line and yield as a named SSE event
hop = parse_hop(line)
if hop:
yield f"event: hop\ndata: {json.dumps(hop)}\n\n"
proc.wait()
# Signal that the trace is complete
yield f"event: done\ndata: {json.dumps({})}\n\n"
What changed from the raw version?
- Instead of yielding raw text lines, the generator now calls parse_hop() on each line and yields the result as JSON.
- Each event now has a named type (event: hop) so the frontend can listen for specific events instead of generic messages.
- A final event: done event tells the browser the trace is finished so it can close the connection cleanly.
- Now update the trace() route to call the new function and add input validation. Find this section in your trace() function:
if not target:
return 'Missing target parameter', 400
return Response(
stream_with_context(run_traceroute_raw(target)),
- Replace those lines with the following, which adds a validation check and calls run_traceroute instead:
if not target:
return 'Missing target parameter', 400
if not re.match(r'^[\w.\-]+$', target):
return 'Invalid target', 400
return Response(
stream_with_context(run_traceroute(target)),
Why add input validation?
The regex ^[\w.\-]+$ ensures the target contains only word characters, dots, and hyphens. This prevents anyone from injecting shell commands through the input field. Combined with the list-based Popen call (no shell=True), this gives you two layers of protection.
- Save app.py.
- Stop the Flask server if it is running (press Ctrl+C in the terminal) and restart it by running:
python3 app.py
The server should start again on http://127.0.0.1:5000. The frontend still shows the old raw display, but the backend is now emitting JSON. You will fix the frontend next.
Seeing a SyntaxError or NameError?
Check that you removed the entire old run_traceroute_raw function and that no leftover lines remain. Also make sure import json and import re are at the very top of the file.
help me fix a SyntaxError or NameError in my Flask traceroute app after replacing the generator function
Update the frontend to parse JSON
The backend now emits named hop events with JSON data. The browser's EventSource needs to listen for those named events (instead of the generic onmessage handler) and render structured data in a table.
- Replace the entire contents of templates/index.html with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Traceroute Visualizer</h1>
<input type="text" id="target" placeholder="Enter hostname or IP">
<button id="trace-btn">Trace</button>
<table id="hop-table" border="1">
<thead><tr><th>Hop</th><th>IP</th><th>Host</th><th>RTT (ms)</th></tr></thead>
<tbody id="hop-body"></tbody>
</table>
<script>
document.getElementById('trace-btn').addEventListener('click', function() {
const target = document.getElementById('target').value.trim();
if (!target) return;
document.getElementById('hop-body').innerHTML = '';
const es = new EventSource('/trace?target=' + encodeURIComponent(target));
es.addEventListener('hop', function(e) {
const hop = JSON.parse(e.data);
const row = document.createElement('tr');
row.innerHTML = '<td>' + hop.hop + '</td><td>' + (hop.ip || '*') + '</td><td>' + (hop.host || '*') + '</td><td>' + (hop.rtt !== null ? hop.rtt : '*') + '</td>';
document.getElementById('hop-body').appendChild(row);
});
es.addEventListener('done', function() { es.close(); });
es.onerror = function() { es.close(); };
});
</script>
</body>
</html>
What changed in the frontend?
- The <pre> element is replaced with a <table> that has columns for hop number, IP, hostname, and RTT.
- es.addEventListener('hop', ...) listens specifically for the named hop event type. Named events are invisible to onmessage.
- JSON.parse(e.data) converts each incoming event payload into a JavaScript object with hop, ip, host, and rtt fields.
- The done event listener cleanly closes the EventSource connection when the trace finishes.
- Save templates/index.html.
Before you test, what do you think will appear in the table when you trace a target? Will every row have an IP address, or might some rows show asterisks?
- Refresh your browser at http://127.0.0.1:5000.
- Type google.com into the input field and click Trace.
You should see table rows appearing one-by-one. Each row shows the hop number, an IP address (or * for timeouts), the hostname, and the RTT in milliseconds. Some hops will show * in all fields. Those are routers that silently drop the probe packets.
Why do some hops time out?
Many routers are configured to not respond to ICMP or UDP probes. They still forward your actual traffic fine. The * * * output means the router did not send a reply back before the timeout elapsed.
Table not populating?
Open the browser's DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows) and check the Console tab for errors. A common issue is a mismatch between the event name in the backend (event: hop) and the listener name in JavaScript ('hop'). They must match exactly.
If the Console shows no errors at all, switch to the Network tab and look for the /trace request. Click it and check the EventStream tab to see if JSON events are arriving from the backend.
help me debug why my EventSource hop events are not showing in the table
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and templates/index.html.
ⓧ I'd like to double check the full code
Here is the full app.py at this point. Scroll up and confirm these sections appear in order: imports, parse_hop(), run_traceroute(), routes, and the __main__ block.
app.py (imports and parse_hop):
import json
import re
import subprocess
from flask import Flask, Response, render_template, request, stream_with_context
app = Flask(__name__)
def parse_hop(line):
"""Parse a single traceroute output line into a structured dict."""
hop_match = re.match(r'\s*(\d+)\s+(.+)', line)
if not hop_match:
return None
hop_num = int(hop_match.group(1))
rest = hop_match.group(2)
if re.match(r'^[\s\*]+$', rest):
return {'hop': hop_num, 'ip': None, 'host': None, 'rtt': None}
host_ip_match = re.search(r'(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)', rest)
host = None
ip = None
if host_ip_match:
host = host_ip_match.group(1)
ip = host_ip_match.group(2)
else:
ip_only_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', rest)
if ip_only_match:
ip = ip_only_match.group(1)
host = ip
app.py (RTT extraction and run_traceroute):
rtt_values = re.findall(r'([\d.]+)\s+ms', rest)
rtt = None
if rtt_values:
rtt = round(sum(float(r) for r in rtt_values) / len(rtt_values), 2)
return {'hop': hop_num, 'ip': ip, 'host': host, 'rtt': rtt}
def run_traceroute(target):
"""Generator that runs traceroute and yields parsed hop events."""
proc = subprocess.Popen(
['traceroute', '-m', '20', '-q', '1', target],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
while True:
line = proc.stdout.readline()
if line == '' and proc.poll() is not None:
break
if line.strip():
hop = parse_hop(line)
if hop:
yield f"event: hop\ndata: {json.dumps(hop)}\n\n"
proc.wait()
yield f"event: done\ndata: {json.dumps({})}\n\n"
app.py (routes):
@app.route('/')
def index():
return render_template('index.html')
@app.route('/trace')
def trace():
target = request.args.get('target', '').strip()
if not target:
return 'Missing target parameter', 400
if not re.match(r'^[\w.\-]+$', target):
return 'Invalid target', 400
return Response(
stream_with_context(run_traceroute(target)),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
)
if __name__ == '__main__':
app.run(debug=True)
templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Traceroute Visualizer</title>
</head>
<body>
<h1>Traceroute Visualizer</h1>
<input type="text" id="target" placeholder="Enter hostname or IP">
<button id="trace-btn">Trace</button>
<table id="hop-table" border="1">
<thead><tr><th>Hop</th><th>IP</th><th>Host</th><th>RTT (ms)</th></tr></thead>
<tbody id="hop-body"></tbody>
</table>
<script>
document.getElementById('trace-btn').addEventListener('click', function() {
const target = document.getElementById('target').value.trim();
if (!target) return;
document.getElementById('hop-body').innerHTML = '';
const es = new EventSource('/trace?target=' + encodeURIComponent(target));
es.addEventListener('hop', function(e) {
const hop = JSON.parse(e.data);
const row = document.createElement('tr');
row.innerHTML = '<td>' + hop.hop + '</td><td>' + (hop.ip || '*') + '</td><td>' + (hop.host || '*') + '</td><td>' + (hop.rtt !== null ? hop.rtt : '*') + '</td>';
document.getElementById('hop-body').appendChild(row);
});
es.addEventListener('done', function() { es.close(); });
es.onerror = function() { es.close(); };
});
</script>
</body>
</html>
Your traceroute data is now structured and flowing as JSON. The raw text chaos has been tamed into clean hop objects. Next up, you will turn those objects into a live interactive graph where each hop becomes a visible node on screen.
Build the Live Network Graph with Cytoscape.js
Your Flask backend now streams structured JSON hop data to the browser. The data is there, but reading a table of IPs and latencies doesn't tell you much about the actual path your packets travel.
In this step, you will transform that data stream into a live, interactive network graph. Each hop appears as a color-coded node the moment it arrives, revealing the route packets take and where the network slows down or goes dark.
In this step, get ready to:
- Set up a dark-themed dashboard with the Cytoscape.js graph library loaded from CDN.
- Build a function that adds nodes and edges to the graph as hop data arrives in real time.
- Wire up the Trace and Clear buttons so you can run and reset traces from the UI.
Set up the Cytoscape.js canvas
Right now your index.html is a bare HTML page with a table. You are going to replace it entirely with a dark-themed dashboard that loads Cytoscape.js from a CDN and creates a dedicated graph container.
- Open templates/index.html in VS Code.
- Select all the existing content and delete it.
- Paste in the new HTML structure below. This sets up the dark theme, the input controls, a status indicator, and the graph container:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Traceroute Visualizer</title>
<script src="https://unpkg.com/cytoscape@3.34.0/dist/cytoscape.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { text-align: center; margin-bottom: 20px; color: #00d4ff; }
.controls { display: flex; gap: 10px; justify-content: center; margin-bottom: 20px; }
input[type="text"] { padding: 10px 16px; font-size: 16px; border: 1px solid #333; border-radius: 6px; background: #16213e; color: #e0e0e0; width: 300px; }
button { padding: 10px 20px; font-size: 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }
#trace-btn { background: #00d4ff; color: #1a1a2e; }
#clear-btn { background: #333; color: #e0e0e0; }
#status { text-align: center; margin-bottom: 10px; font-size: 14px; color: #888; }
#cy { width: 100%; height: 400px; background: #16213e; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px; }
</style>
</head>
<body>
<h1>Traceroute Visualizer</h1>
<div class="controls">
<input type="text" id="target" placeholder="Enter hostname or IP (e.g. 8.8.8.8)">
<button id="trace-btn">Trace</button>
<button id="clear-btn">Clear</button>
</div>
<div id="status">Ready</div>
<div id="cy"></div>
<script>
What does this code do?
The <script> tag in the head loads Cytoscape.js 3.34.0 from unpkg CDN. No install needed.
The #cy div is the graph container. Cytoscape.js requires its container to have explicit CSS dimensions before initialization, which is why we set width: 100% and height: 400px in the stylesheet.
The #status div will show live progress as hops arrive. The Clear button lets you reset the graph between traces.
- Directly below the opening <script> tag, paste the Cytoscape initialization and the color helper function:
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{ selector: 'node', style: { 'label': 'data(label)', 'text-valign': 'bottom', 'text-margin-y': 8, 'font-size': '10px', 'color': '#ccc', 'background-color': 'data(color)', 'width': 30, 'height': 30 } },
{ selector: 'edge', style: { 'width': 2, 'line-color': '#555', 'target-arrow-color': '#555', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier' } }
],
layout: { name: 'preset' }
});
let eventSource = null;
let hopCount = 0;
function getColor(rtt) {
if (rtt === null) return '#666';
if (rtt < 20) return '#00e676';
if (rtt < 100) return '#ffab00';
return '#ff1744';
}
What does this code do?
The cytoscape() call creates the graph instance, pointing it at your #cy container. The style array tells Cytoscape.js to read each node's color and label from its data object using the data() mapper syntax.
The getColor() function maps RTT values to colors: green for fast (under 20ms), yellow for moderate (20-100ms), red for slow (over 100ms), and grey for timeouts.
- Save templates/index.html.
- Return to the terminal running your Flask app, stop it with Ctrl+C, and restart it by running:
python3 app.py
- Refresh http://127.0.0.1:5000 in your browser. You should see a dark-themed page with a cyan heading, a text input, two buttons (Trace and Clear), a "Ready" status line, and a dark rounded rectangle where the graph will render.
Seeing a blank white page?
- Check that you deleted the old HTML before pasting. Any leftover <table> tags from the previous version will break the layout.
- Make sure the <script> tag loading the CDN is inside <head>. If it fails to load, your browser console will show a 404 error.
- help me debug my Cytoscape.js canvas setup
Add nodes and edges live as hops arrive
With the canvas ready, you need a function that takes a parsed hop object and turns it into a visible node on the graph. Each time a new hop arrives, the function adds a node, connects it to the previous hop with an edge, and re-runs the layout so the graph arranges itself.
- In templates/index.html, find the closing brace of the getColor function. Directly below it, paste the addHop function:
function addHop(hop) {
hopCount++;
const nodeId = 'hop-' + hop.hop;
const label = hop.host || hop.ip || '* * *';
const color = getColor(hop.rtt);
cy.add({ data: { id: nodeId, label: label.length > 20 ? label.substring(0, 18) + '...' : label, color: color } });
if (hopCount > 1) {
cy.add({ data: { id: 'edge-' + hop.hop, source: 'hop-' + (hop.hop - 1), target: nodeId } });
}
cy.layout({ name: 'breadthfirst', directed: true, spacingFactor: 1.2, animate: true, animationDuration: 300 }).run();
document.getElementById('status').textContent = 'Tracing... Hop ' + hop.hop + ' - ' + (hop.ip || 'timeout');
}
What does this code do?
- Each node gets an ID like hop-1, hop-2, etc. The label shows the hostname, IP, or * * * for timeouts.
- Edges connect each hop to the previous one, building a linear path that mirrors the actual packet route.
- After every addition, cy.layout({name: 'breadthfirst'}).run() re-arranges all nodes in a hierarchical tree layout. This is what makes the path grow visually rather than stacking nodes on top of each other.
- The #status div updates with each hop so you can see progress even before the graph fully renders.
Wire up the trace and clear controls
The addHop function exists but nothing calls it yet. You need a startTrace function that opens the EventSource connection and routes incoming hops to the graph, plus a clearGraph function that resets everything for a fresh trace.
- Below the addHop function, paste the remaining JavaScript and closing tags:
function startTrace() {
const target = document.getElementById('target').value.trim();
if (!target) return;
clearGraph();
document.getElementById('status').textContent = 'Starting trace to ' + target + '...';
eventSource = new EventSource('/trace?target=' + encodeURIComponent(target));
eventSource.addEventListener('hop', function(e) { addHop(JSON.parse(e.data)); });
eventSource.addEventListener('done', function() {
document.getElementById('status').textContent = 'Trace complete - ' + hopCount + ' hops';
eventSource.close();
});
eventSource.onerror = function() { document.getElementById('status').textContent = 'Connection lost'; eventSource.close(); };
}
function clearGraph() {
cy.elements().remove();
document.getElementById('status').textContent = 'Ready';
hopCount = 0;
if (eventSource) { eventSource.close(); eventSource = null; }
}
document.getElementById('trace-btn').addEventListener('click', startTrace);
document.getElementById('clear-btn').addEventListener('click', clearGraph);
document.getElementById('target').addEventListener('keypress', function(e) { if (e.key === 'Enter') startTrace(); });
</script>
</body>
</html>
What does this code do?
- startTrace() clears any existing graph, opens an EventSource connection to /trace, and routes each hop event to addHop().
- The done event listener updates the status to show the final hop count and closes the connection.
- clearGraph() removes all nodes and edges from the Cytoscape instance, resets the hop counter, and closes any active EventSource connection.
- The keypress listener on the input field lets you press Enter to start a trace instead of clicking the button.
- Save templates/index.html.
- Restart the Flask server (Ctrl+C, then python3 app.py).
Before you test, think about what you expect to see. The trace will send UDP probes with incrementing TTL values, one hop at a time. What should the graph look like as each probe returns?
- Refresh http://127.0.0.1:5000, type 8.8.8.8 in the input field, and click Trace.
You should see nodes appearing one-by-one in the graph, connected by edges, forming a path from left to right. Nodes near your router will be green (low latency). Nodes further out will shift to yellow or red. Any timeout hops appear as grey nodes labeled * * *. When the trace finishes, the status shows "Trace complete" with the total hop count.
- Click Clear and confirm the graph empties and the status resets to "Ready".
Nodes not appearing after clicking Trace?
- Open your browser's developer console (Cmd+Option+J on macOS). If you see "cytoscape is not defined", the CDN script failed to load. Check the URL in your <head> matches exactly: https://unpkg.com/cytoscape@3.34.0/dist/cytoscape.min.js.
- If you see "Cannot read properties of null" referencing the container, make sure your <div id="cy"> appears BEFORE the <script> block in the HTML. Cytoscape.js needs the container to exist in the DOM at initialization time.
- If the status updates but the graph stays empty, check that cy.add() is passing the data object with id, label, and color fields.
- help me debug why my Cytoscape.js graph is not rendering nodes
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/index.html and the graph is rendering nodes when you trace.
ⓧ I'd like to double check the full code
Your templates/index.html file should contain these sections in order. Scroll up and confirm each one is present:
- HTML <head> with the Cytoscape.js CDN <script> tag and the full <style> block (dark theme, controls, #cy container).
- HTML <body> with the h1, .controls div (input + two buttons), #status div, and #cy div.
- Cytoscape initialization with node and edge styles, eventSource and hopCount variables, and the getColor() function.
- The addHop() function that adds nodes/edges and re-runs the breadthfirst layout.
- The startTrace() function, clearGraph() function, and three event listeners (trace-btn click, clear-btn click, target keypress).
- Closing </script></body></html> tags.
The app.py file is unchanged from the previous step.
Your traceroute data now paints a live picture of the network path. In the Secret Mission, you will add a latency bar chart that builds alongside the graph, giving you a dual-visualization dashboard.
Secret mission
Add a Live Latency Bar Chart
The graph shows the path your packets take, but it does not make relative latency obvious at a glance. Can you add a live-updating bar chart below the graph where each bar represents a hop, colored by speed, and rescales dynamically as new hops arrive?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future experimentation or delete them entirely.
Resources you used:
- The traceroute-visualizer project folder (contains app.py, templates/index.html, and the venv virtual environment with Flask installed).
Keep everything running
No action needed. Choose this if you want to keep experimenting with traceroute targets or extending the visualizer.
- The Flask server only runs when you start it manually with python3 app.py. There are no background processes or costs.
- Your virtual environment stays dormant until you activate it again.
Pause - I'll come back to this later
Stop any running processes to free up your terminal. Your files stay on disk for when you return.
- If the Flask dev server is still running, press Ctrl+C in the terminal to stop it.
- Deactivate your virtual environment by running:
deactivate
When you come back, reactivate with source venv/bin/activate from inside the traceroute-visualizer folder.
Delete - I don't want to use this again
Remove all project files. Deleting the project folder also removes the virtual environment and Flask installation inside it.
- If the Flask dev server is still running, press Ctrl+C to stop it.
- Deactivate the virtual environment if it is active:
deactivate
- Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf traceroute-visualizer
- Confirm the folder is gone by running:
ls | grep traceroute
You should see no output. The project is fully removed.
Nice Work!
Nice Work!
You just built a real-time traceroute visualizer that turns an invisible network path into a live, interactive dashboard. That is a serious networking tool running entirely on your local machine.
Here's what you learned:
- Built a Flask web server that spawns a system process with subprocess.Popen and streams its output to the browser using Server-Sent Events.
- Wrote a regex parser that transforms raw traceroute output into structured JSON, extracting hop numbers, IPs, hostnames, and RTT values from each line.
- Created an interactive real-time network graph using Cytoscape.js that visually maps the path packets take across the internet, with nodes color-coded by latency.
- Secret Mission: Added a live-updating latency bar chart using pure HTML/CSS, giving you a dual-visualization dashboard that builds alongside the graph.
Ready to quiz yourself?