Traceroutes (MTR)
Agents collect MTR traceroutes automatically for every ICMP target. How often depends on your plan. Each report records the path from one agent to the target, with packet loss and latency for every hop. Observiant adds each hop's reverse DNS name, autonomous system and country when it receives the report.
Each report traces up to 50 hops and sends 10 probes to each hop. The agent uses the target's address family and resolves hostnames itself, so the destination in a report is the address that agent actually reached.
The endpoints on this page let you:
- list reports for a target
- pick representative reports for a time window, for charts and timelines
- find the report closest to a moment, such as a latency spike
- fetch a full report
Traceroutes are also the input for path changes and correlated events.
List traceroutes
GET /api/targets/{target_id}/mtr
Returns report summaries for a target, newest first.
| Parameter | Description |
|---|---|
agent_label | Optional. Only reports from this agent. |
start | Optional. Earliest report time, Unix milliseconds. |
end | Optional. Latest report time, Unix milliseconds. |
limit | Optional. Maximum number of reports, 1–500. Default 50. Larger values are reduced to 500. |
Without start and end, the newest reports in the retention period are returned. When you give both, the range can be at most 30 days.
- curl
- Python
curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/mtr?agent_label=sys-amsterdam&limit=2"
response = session.get(
f"{base_url}/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/mtr",
params={"agent_label": "sys-amsterdam", "limit": 2},
timeout=30,
)
response.raise_for_status()
results = response.json()["results"]
{
"results": [
{
"result_id": "01M4N2XQ8ZKJ5W0D6V3T9R1B4E",
"target_id": "01M4MZ5TCN74PFRTXDKXYE4XWX",
"agent_label": "sys-amsterdam",
"display_name": "Amsterdam",
"timestamp": 1791706847120,
"dst": "8.8.8.8",
"hop_count": 6,
"destination_reached": true,
"as_path": ["AS64500", "AS64501", "AS15169"]
},
{
"result_id": "01M4N2M3F1T7Q8H2C5Y0N6X9WA",
"target_id": "01M4MZ5TCN74PFRTXDKXYE4XWX",
"agent_label": "sys-amsterdam",
"display_name": "Amsterdam",
"timestamp": 1791706472316,
"dst": "8.8.8.8",
"hop_count": 6,
"destination_reached": true,
"as_path": ["AS64500", "AS64501", "AS15169"]
}
]
}
| Field | Description |
|---|---|
result_id | Report ID. Use it with Get a traceroute report. |
target_id, agent_label, display_name | The target and the agent that ran the traceroute. |
timestamp | When the traceroute started, Unix milliseconds. |
dst | Destination address the agent traced to. |
hop_count | Number of hops in the report. |
destination_reached | true when the last hop is the destination address. |
as_path | Autonomous systems along the path, in order, without repeats and without hops whose AS is unknown. |
An unknown target, or a target without reports, returns an empty results array.
Fetch older reports
To page backwards through history, repeat the request with end set to one millisecond before the oldest timestamp you received. Stop when a response contains fewer reports than limit.
def all_reports(session, base_url, target_id, start, end):
"""Yield every report summary in [start, end], newest first. The range must be 30 days or less."""
while True:
response = session.get(
f"{base_url}/api/targets/{target_id}/mtr",
params={"start": start, "end": end, "limit": 500},
timeout=30,
)
response.raise_for_status()
results = response.json()["results"]
yield from results
if len(results) < 500:
return
end = results[-1]["timestamp"] - 1
Get representative traceroutes
GET /api/targets/{target_id}/mtr/markers
Returns a bounded selection of reports that covers a whole time window evenly. Use it to annotate a chart or timeline without downloading every report. The window is divided into equal intervals, and at most one report is returned for each. Within an interval, Observiant picks the most informative report, in this order:
- a report where a path change was detected
- a report with packet loss at the destination, or one that did not reach it
- any other report
When several reports qualify equally, the newest one is returned.
| Parameter | Description |
|---|---|
start | Required. Window start, Unix milliseconds. |
end | Required. Window end, Unix milliseconds. The window can be at most 366 days. |
agent_label | Optional. Only reports from this agent. |
limit | Optional. Maximum number of reports, 1–500. Default 300. |
END=$(date +%s000)
START=$(( END - 86400000 )) # last 24 hours
curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/mtr/markers?start=$START&end=$END&limit=100"
{
"markers": [
{
"result_id": "01M4N1G7B2V9K3D8R5W0T6Y4QH",
"timestamp": 1791702011845,
"dst": "8.8.8.8",
"hop_count": 7,
"destination_reached": true,
"destination_loss_pct": 20,
"has_packet_loss": true,
"has_event": true
}
]
}
Markers are returned oldest first. The marker fields that differ from the list summary are:
| Field | Description |
|---|---|
destination_loss_pct | Packet loss at the destination, percent. 100 when the destination was not reached. |
has_packet_loss | true when the destination had packet loss or was not reached. |
has_event | true when this report confirmed a path change. |
When you zoom into part of the window, request markers again with the narrower start and end to get a denser selection. A missing start or end returns 400 Bad Request.
Get the traceroute closest to a time
GET /api/targets/{target_id}/mtr/nearest
Returns the full report whose start time is closest to timestamp, before or after it. Combined with latency and loss, this lets you go from a spike in a chart to the path at that moment.
| Parameter | Description |
|---|---|
timestamp | Required. Unix milliseconds. |
agent_label | Optional. Only consider reports from this agent. Recommended, because different agents take different paths. |
curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/mtr/nearest?timestamp=1791706800000&agent_label=sys-amsterdam"
The response has the same format as Get a traceroute report. If the target, or the agent, has no reports, the response is 404 Not Found with {"error": "No MTR results for this target"}. The closest report can be far from timestamp, so check its timestamp before you rely on it.
Get a traceroute report
GET /api/mtr/{result_id}
Returns one complete report. result_id comes from the list, markers, nearest, path change and event responses.
- curl
- Python
curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/mtr/01M4N2XQ8ZKJ5W0D6V3T9R1B4E"
response = session.get(f"{base_url}/api/mtr/01M4N2XQ8ZKJ5W0D6V3T9R1B4E", timeout=30)
response.raise_for_status()
report = response.json()
for hop in report["report"]["hubs"]:
print(f"{hop['count']:>2} {hop['host']:<15} {hop['host_asn']:<8} "
f"loss {hop['loss_pct']:5.1f}% avg {hop['avg']:6.1f} ms {hop['hostname']}")
1 192.0.2.1 loss 0.0% avg 0.4 ms
2 198.51.100.17 AS64500 loss 0.0% avg 1.2 ms ae1-10.ams-core1.example.net
3 198.51.100.66 AS64500 loss 0.0% avg 1.9 ms be20.ams-edge2.example.net
4 ??? loss 100.0% avg 0.0 ms
5 203.0.113.9 AS64501 loss 0.0% avg 2.8 ms
6 8.8.8.8 AS15169 loss 0.0% avg 2.6 ms dns.google
{
"result_id": "01M4N2XQ8ZKJ5W0D6V3T9R1B4E",
"target_id": "01M4MZ5TCN74PFRTXDKXYE4XWX",
"agent_label": "sys-amsterdam",
"display_name": "Amsterdam",
"timestamp": 1791706847120,
"report": {
"mtr": {
"src": "198.51.100.200",
"dst": "8.8.8.8",
"tos": 0,
"tests": 10,
"psize": "64",
"bitpattern": "0x00"
},
"hubs": [
{"count": 1, "host": "192.0.2.1", "hostname": "", "host_country": "", "host_asn": "",
"loss_pct": 0, "snt": 10, "last": 0.41, "avg": 0.44, "best": 0.38, "wrst": 0.61, "stdev": 0.07},
{"count": 2, "host": "198.51.100.17", "hostname": "ae1-10.ams-core1.example.net", "host_country": "NL", "host_asn": "AS64500",
"loss_pct": 0, "snt": 10, "last": 1.18, "avg": 1.22, "best": 1.1, "wrst": 1.47, "stdev": 0.11},
{"count": 3, "host": "198.51.100.66", "hostname": "be20.ams-edge2.example.net", "host_country": "NL", "host_asn": "AS64500",
"loss_pct": 0, "snt": 10, "last": 1.87, "avg": 1.93, "best": 1.81, "wrst": 2.25, "stdev": 0.13},
{"count": 4, "host": "???", "hostname": "", "host_country": "", "host_asn": "",
"loss_pct": 100, "snt": 10, "last": 0, "avg": 0, "best": 0, "wrst": 0, "stdev": 0},
{"count": 5, "host": "203.0.113.9", "hostname": "", "host_country": "NL", "host_asn": "AS64501",
"loss_pct": 0, "snt": 10, "last": 2.79, "avg": 2.84, "best": 2.71, "wrst": 3.06, "stdev": 0.1},
{"count": 6, "host": "8.8.8.8", "hostname": "dns.google", "host_country": "US", "host_asn": "AS15169",
"loss_pct": 0, "snt": 10, "last": 2.62, "avg": 2.64, "best": 2.55, "wrst": 2.9, "stdev": 0.09}
]
}
}
Report fields
| Field | Description |
|---|---|
result_id, target_id, agent_label, display_name, timestamp | As in the list summary. |
report.mtr.src | The agent's address as seen by Observiant. Behind NAT this is the translated public address, not the address of the agent's interface. |
report.mtr.dst | Destination address. |
report.mtr.tos | IP type-of-service value of the probes. |
report.mtr.tests | Number of probe cycles sent to each hop. |
report.mtr.psize | Probe packet size in bytes, as a string. |
report.mtr.bitpattern | Byte used to fill the probe payload, as a hexadecimal string such as 0x00. |
report.hubs | Hops in path order. The first element is hop 1. |
Each hop has these fields:
| Field | Description |
|---|---|
count | Hop number, starting at 1. |
host | Address that answered at this hop, or ??? when no reply was received. |
hostname | Reverse DNS name of host when the report was received, or an empty string. |
host_asn | Autonomous system of host, such as AS64500, or an empty string. |
host_country | ISO 3166-1 alpha-2 country code of host, or an empty string. |
loss_pct | Percentage of probes to this hop that got no reply. |
snt | Number of probes sent to this hop. |
last, avg, best, wrst | Last, average, best and worst round-trip time, milliseconds. |
stdev | Standard deviation of the round-trip time, milliseconds. |
The report follows the JSON format of the mtr tool.
Reading loss in a report
Loss at an intermediate hop is not always a problem. Many routers rate-limit the ICMP replies they generate themselves while forwarding traffic normally. Treat loss as real forwarding loss when it starts at a hop and continues through every later hop to the destination. A loss figure that appears at one hop and disappears at the next is usually rate limiting. Correlated events apply this rule automatically across targets and agents.