I actually tried to observe satellite handover on Starlink Mini
This page has been translated by machine translation. View original
I am Okuri, who loves whiskey, cigars, and pipes above all else.
Through recent conversations with customers, I have come to think that satellite links could be an option when high-speed communications are needed in remote locations. LEO (Low Earth Orbit) satellites like Starlink orbit at low altitude, which keeps latency small and allows them to be used with a feel close to terrestrial connections.
On the other hand, because LEO satellites pass overhead in a matter of minutes, handovers that switch between satellites during a communication session are unavoidable. Research reports indicate that Starlink performs these switches every 15 seconds. I was curious about the actual impact, so I ran measurements for one hour between a Starlink Mini on hand and an EC2 instance in the Tokyo region.
Starlink's 15-Second Cycle
The paper presented at CoNEXT 2023 cited above measured four Starlink terminals placed in the United States and Europe at high frequency and analyzed the behavior of Starlink's scheduling. There are two key points.
- Starlink uses a centralized control scheme that globally updates satellite selection, routes, and flow tables on a 15-second cycle
- The boundaries are aligned near the 12-second, 27-second, 42-second, and 57-second marks of each minute
From the terminal's perspective, this means the satellite and beam handling the communication may switch every 15 seconds. At the moment of a switch, latency degradation and brief packet loss should occur, and after the switch the base latency itself should change because traffic is now routed through a different satellite. Confirming these two points through actual measurement is the purpose of this exercise.
Measurement Setup
The setup is as follows. A Mac acting as the measurement client is connected via wired connection to the Starlink Mini, and measurement packets are sent continuously toward an EC2 instance in the Tokyo region.
[Mac (wired LAN)] Measurement client: irtt client / ping / starlink-grpc-tools dish_grpc_text.py (terminal statistics)
|
| USB Ethernet
v
[Starlink Mini] 192.168.100.1:9200 (gRPC)
|
| Satellite communication
v
[Starlink PoP (Tokyo)]
|
| Internet
v
[EC2 ap-northeast-1] Measurement server
irtt server
Three types of data are collected.
| Data | Source | Granularity | What it shows |
|---|---|---|---|
| Terminal statistics (pop_ping_latency_ms / pop_ping_drop_rate) | Starlink terminal gRPC API | 1 second | Quality from antenna to PoP |
| irtt (RTT / one-way delay / loss direction) | UDP from Mac to EC2 | 100 ms | End-to-end communication status |
| ping (RTT / loss) | ICMP from Mac to EC2 | 200 ms | Confirmation of irtt results |
The Starlink terminal exposes a gRPC API on the LAN side, and using starlink-grpc-tools allows you to extract statistics at 1-second granularity. Note, however, that this is not an officially documented API and may change with firmware updates.
irtt is a tool that sends packets at equal intervals over UDP and records one-way delay and loss direction in addition to RTT. Since behavior on a satellite link may differ between uplink and downlink, irtt was used as the primary tool rather than ping.
There are three key points about the setup.
- Choosing the Tokyo region: Starlink's PoP in Japan is located in Otemachi, Tokyo. Placing the EC2 instance in Tokyo as well minimizes the latency of the terrestrial segment, making the majority of the measured values attributable to the satellite segment.
- Using a wired connection: If Wi-Fi jitter were mixed in, it would become impossible to distinguish from variation originating from the satellite.
- Initiating measurement from the Starlink side: Since the subscription is ROAM - 100GB, there is no public IP address, and the device sits behind CGNAT, making IPv4 inbound connections impossible. All measurements are initiated outbound from the Mac to EC2.
The prerequisites are as follows.
| Item | Value |
|---|---|
| Satellite antenna | Starlink Mini (mini1_pez_proto1) |
| Firmware | 2026.08.31.mr85832 |
| Installation | Outdoors, clear weather, no obstructions at a location in Yokohama, Kanagawa Prefecture (obstructionStats.fractionObstructed is 0, no alerts) |
| Measurement date/time | September 13, 2026, 14:45–15:47 JST (no load) |
| Measurement client | Apple M4 (macOS 26.6.2), wired connection via USB Ethernet |
| EC2 | ap-northeast-1a (apne1-az4), m8a.xlarge, Ubuntu 24.04 |
| Protocol | IPv4 |
This verification was conducted under no-load (no traffic) conditions without data transfer. Behavior under high load with heavy bandwidth consumption is outside the scope.
The Starlink Mini was actually placed like this.

When I started preparing, the weather was like this, slightly cloudy, but it was mostly clear by the time measurements began.

Doing It
Preparing the EC2 Side
On the EC2 side, all that is needed is to start an irtt server. The Security Group is opened only for the duration of the measurement, restricted to the IP address confirmed via https://checkip.amazonaws.com.
Install the required tools and synchronize the time.
$ sudo apt update && sudo apt install -y irtt iperf3 chrony tmux
$ chronyc tracking
$ tmux new -d -s irtt 'irtt server -i 10ms -d 3h -l 1500'
$ tmux new -d -s iperf 'iperf3 -s'
-i 10ms is the minimum send interval permitted for clients, and -d 3h is the maximum test duration. Without adjusting these from their defaults, long-duration, short-interval measurements would be rejected by the server, so the limits are relaxed.
Preparing the Mac Side
Install measurement tools via Homebrew. Since irtt does not appear to have a Homebrew formula, it is installed with go install.
$ brew install grpcurl iperf3 mtr tmux go python@3.12
$ go install github.com/heistp/irtt/cmd/irtt@latest
$ echo 'export PATH="$PATH:$HOME/go/bin:/opt/homebrew/sbin"' >> ~/.zshrc && source ~/.zshrc
Clone starlink-grpc-tools and install dependencies into a venv.
$ git clone https://github.com/sparky8512/starlink-grpc-tools.git
$ cd starlink-grpc-tools
$ python3 -m venv .venv && source .venv/bin/activate
$ pip install --upgrade -r requirements.txt
$ pip install pandas matplotlib # for analysis
Synchronize the time.
sudo sntp -sd time.apple.com
sntp time.apple.com
Connect the Mac to the Starlink Mini's LAN port via wired connection and confirm that the gRPC API is reachable. Calling get_status with grpcurl returns the terminal status as JSON.
$ grpcurl -plaintext -d '{"get_status":{}}' 192.168.100.1:9200 SpaceX.API.Device.Device/Handle
{
"apiVersion": "43",
"dishGetStatus": {
"deviceInfo": {
"id": "1234567890-12345678-12345678",
"hardwareVersion": "mini1_pez_proto1",
"softwareVersion": "2026.08.31.mr85832",
"countryCode": "JP",
...
},
"deviceState": {
"uptimeS": "623"
},
"obstructionStats": {
"validS": 577,
"patchesValid": 2114
},
"alerts": {},
"downlinkThroughputBps": 10292.822,
"uplinkThroughputBps": 53737.42,
"popPingLatencyMs": 25.54012,
"boresightElevationDeg": 81.441124,
...
}
}
popPingLatencyMs is the RTT from the terminal to the PoP. Under no load, it was around 25 ms. The reason fractionObstructed does not appear in obstructionStats is that the value is 0, meaning the installation has no obstructions.
One thing that tripped me up was Cloudflare WARP. It was installed on the client, so I disabled it along with Wi-Fi during the measurement.
Measurement Scripts
A script was prepared to run the three measurements in parallel using tmux. Key points are as follows.
- Launch via
caffeinate -ito prevent the Mac from sleeping - Since macOS
pinglacks Linux's-Dflag (epoch display), use--apple-timeandTZ=UTCto attach a UTC timestamp to each line - Antenna statistics are appended to a CSV in increments using
dish_grpc_text.py -t 60 bulk_history, which outputs only new samples every 60 seconds
run.sh
1-Minute Test Measurement
Before running for one hour, I ran it for just one minute to verify the output.
$ ./run.sh <EC2 IPv4> 1 test
started: /path-to/measure/data/20260913T053901Z_test (duration 1 min)
watch: tmux ls / tail -f /path-to/measure/data/20260913T053901Z_test/irtt_v4.txt
stop: tmux kill-server
In one minute, loss was zero and the median RTT was 27.49 ms. Looking at the RTT time series, there were 4 spikes, neatly aligned just before 05:39:12, 05:39:27, 05:39:42, and 05:39:57. The vertical lines mark positions where "seconds mod 15 = 12."
Min Mean Median Max Stddev
--- ---- ------ --- ------
RTT 16.93ms 28.42ms 27.49ms 100.7ms 7.57ms
send delay 22.44ms 30.88ms 30.32ms 103ms 7.42ms
receive delay -5.87ms -2.46ms -2.82ms 2.41ms 1.52ms
IPDV (jitter) 5.71µs 5.73ms 4ms 72.44ms 7.42ms
send IPDV 1.36µs 5.75ms 3.98ms 72.78ms 7.37ms
receive IPDV 16ns 1.31ms 518µs 7.2ms 1.68ms
send call time 28.3µs 113µs 340µs 31µs
timer error 541ns 265µs 1.12ms 190µs
server proc. time 670ns 1.15µs 8.18µs 1.04µs
duration: 1m0s (wait 302.1ms)
packets sent/received: 598/598 (0.00% loss)
server packets received: 598/598 (0.00%/0.00% loss up/down)
bytes sent/received: 102856/102856
send/receive rate: 13.7 Kbps / 13.7 Kbps
timer stats: 2/600 (0.33%) missed, 0.26% error

In just one minute, the behavior described in the paper was already visible. This seems worth running for a full hour.
Running for 60 Minutes
The full run is 60 minutes. It was executed during the daytime under no-load conditions.
$ ./run.sh <EC2 IPv4> 60 idle-day
started: /path-to/measure/data/20260913T054545Z_idle-day (duration 60 min)
Then just wait one hour at leisure. Since measurements were taken in a spot with a clear view and no obstructions, let's enjoy some whiskey, a cigar, and a pipe.
The irtt summary is as follows. 35,957 packets were sent at 100 ms intervals, with a loss rate of 0.08%.
Min Mean Median Max Stddev
--- ---- ------ --- ------
RTT 15.67ms 30.98ms 28.29ms 183.4ms 9.79ms
send delay 9.72ms 68.5ms 76.77ms 234ms 25.61ms
receive delay -69.96ms -37.52ms -47.47ms 28.15ms 24.4ms
IPDV (jitter) 104ns 6.51ms 4.01ms 158.9ms 8.68ms
send IPDV 120ns 6.08ms 3.75ms 143.8ms 8.45ms
receive IPDV 20ns 1.86ms 639µs 25.71ms 2.48ms
send call time 13.5µs 117µs 1.2ms 26.9µs
timer error 42ns 415µs 2.77ms 309µs
server proc. time 300ns 1.26µs 24.6µs 1.16µs
duration: 1h0m0s (wait 550.3ms)
packets sent/received: 35957/35928 (0.08% loss)
server packets received: 35934/35957 (0.06%/0.02% loss up/down)
late (out-of-order) pkts: 1 (0.00%)
bytes sent/received: 6184604/6179616
send/receive rate: 13.7 Kbps / 13.7 Kbps
timer stats: 42/35999 (0.12%) missed, 0.42% error
Looking at server packets received, loss differs between uplink (antenna to EC2) at 0.06% and downlink (EC2 to antenna) at 0.02%.
Analyzing the Results
An analysis script analyze.py was prepared to cross-reference the three datasets. When given the data directory, it generates a summary.md with overall statistics, per-15-second-phase statistics, and loss run lengths in Markdown table format, along with PNG graphs. The full script is long so it has been placed in a Gist.
analyze.py
$ .venv/bin/python3 analyze.py ~/measure/data/20260913T054545Z_idle-day
Here is an excerpt of the main part. It simply extracts the send timestamp and RTT from the irtt JSON and aggregates by the 15-second phase of the send timestamp (UTC). If the paper's description is correct, a peak should appear near phase 12, which is the boundary.
import json, numpy as np, pandas as pd
j = json.load(open("irtt_v4.json"))
rows = []
for rt in j["round_trips"]:
t = rt["timestamps"]["client"]["send"]["wall"] / 1e9 # send time (UTC epoch seconds)
rtt = rt["delay"]["rtt"] / 1e6 if "rtt" in rt.get("delay", {}) else np.nan
rows.append({"t": t, "rtt_ms": rtt, "lost": rt["lost"] != "false"})
df = pd.DataFrame(rows)
df["phase"] = np.floor(df["t"]).astype("int64") % 15 # seconds mod 15 = phase
print(df.groupby("phase").agg(loss_pct=("lost", lambda s: 100 * s.mean()),
rtt_p50=("rtt_ms", "median"),
rtt_p99=("rtt_ms", lambda s: s.quantile(.99))))
Column names for the terminal statistics CSV can be confirmed with dish_grpc_text.py -H bulk_history. Since the timestamps are in UTC, converting them to the same epoch seconds as irtt allows cross-referencing at one-second resolution.
Results
Overall Numbers
First, the overall statistics for the one hour.
| Measurement | Samples | Loss rate | RTT p50 | RTT p95 | RTT p99 | RTT max |
|---|---|---|---|---|---|---|
| irtt (UDP, end-to-end) | 35,957 | 0.08% | 28.3 ms | 47.9 ms | 68.1 ms | 183.4 ms |
| ping (ICMP, end-to-end) | 17,908 | 0.10% | 25.2 ms | 43.7 ms | 58.1 ms | 145.7 ms |
| Antenna statistics (antenna to PoP, 1-second interval) | 3,663 | 0.03% | 24.4 ms | 31.1 ms | 35.3 ms | 39.9 ms |
Looking at the median, the terminal-to-PoP segment is 24 ms and end-to-end is 25–28 ms. Since the terrestrial segment from the PoP to EC2 adds only 1–4 ms, using the Tokyo region means almost all of the measured value is attributable to the satellite segment.
At p99, the gap between antenna statistics and end-to-end widens. This is because antenna statistics are 1-second averages, which smooth out spikes at the 100 ms scale.
The overall time series is shown below. The top panel shows RTT (blue for irtt, orange for antenna statistics), the middle panel shows loss rate, and the bottom panel shows throughput.

Correlation with the 15-Second Phase
The remainder when the send timestamp (UTC) in seconds is divided by 15 is defined as the "phase," and statistics were computed per phase. If the paper's description is correct, a peak should appear near phase 12, which is the boundary.

| Phase | Samples | Loss rate | RTT p50 | RTT p99 | RTT max |
|---|---|---|---|---|---|
| 9 | 2,399 | 0.04% | 28.7 ms | 57.8 ms | 89.8 ms |
| 10 | 2,397 | 0.04% | 27.5 ms | 55.7 ms | 84.7 ms |
| 11 | 2,399 | 0.33% | 28.1 ms | 96.7 ms | 183.4 ms |
| 12 | 2,398 | 0.13% | 27.5 ms | 57.0 ms | 83.8 ms |
| 13 | 2,397 | 0.08% | 27.5 ms | 54.6 ms | 75.8 ms |
| 14 | 2,398 | 0.17% | 27.7 ms | 55.3 ms | 89.1 ms |
Only phase 11—that is, the one second immediately before the 12-, 27-, 42-, and 57-second marks—is clearly different. The RTT p99 is 96.7 ms compared to 55–70 ms for other phases, and the loss rate is 0.33% compared to 0–0.17% for other phases. Since the p50 is nearly constant at 27–30 ms regardless of phase, the median does not change but the tail becomes heavier.
The reason the peak appears one second before the boundary is that packets sent during phase 11 span the boundary while traveling through the satellite and back. The same peak at phases 11 and 12 appears in ping (orange) as well, confirming this is not an irtt-specific phenomenon.
Zooming In for a Closer Look
The figure below is a close-up of the two-minute window with the worst loss and p99. The vertical lines mark the 15-second boundaries (seconds mod 15 = 12).

This figure reveals a characteristic pattern. Not only are there boundary spikes, but the RTT level shifts every 15 seconds. At 05:48:12 it enters a segment with scattered values between 45–60 ms; at 05:48:27 it snaps cleanly to a flat 27 ms; at 05:48:42 the scatter returns. Every 15 seconds the terminal is reassigned to a different satellite or beam, and the RTT level differs depending on the distance to that satellite and its congestion level. The orange antenna statistics (1-second averages) also gently track the same steps.
How Long Does Communication Actually Drop?
When communication drops during a handover, how long does it actually drop for? And which direction—uplink or downlink—experiences the loss? These two questions are examined using the irtt results.
First, the number of dropped packets and the number of outage events are counted separately. Since packets are sent at 100 ms intervals, grouping consecutive dropped packets into a single outage event reveals the duration of each outage.
| Item | Value |
|---|---|
| Packets sent | 35,957 |
| Dropped packets | 29 (0.08%) |
| Number of outage events (consecutive losses counted as one) | 26 |
| Of which 2 consecutive packets (approx. 200 ms) | 3 |
| 3 or more consecutive packets (300 ms or more) | 0 |
| Dropped packets falling within phase 11 or 12 | 11 (37.9%) |
The maximum outage duration was 200 ms—meaning at most 2 consecutive packets at 100 ms intervals were dropped—and over the 60-minute period, outages never exceeded 1 second.
Looking at the phase of dropped packets, just 2 of the 15 phases—phases 11 and 12—account for 38% of all drops. Just as with the RTT spikes seen in the previous section, loss is also concentrated at the handover boundaries.
Next, the direction is examined. irtt determines per packet: if the packet does not reach the server, it is an uplink loss (Mac to EC2); if the server received it but it did not return to the Mac, it is a downlink loss (EC2 to Mac).
| Direction | Dropped packets |
|---|---|
| Uplink (Mac to EC2) | 23 |
| Downlink (EC2 to Mac) | 6 |
Loss was skewed toward the uplink. The direction from the terminal to the satellite appears to be more susceptible to the effects of switching. That said, the overall loss rate is 0.08%, which is a level that can be considered unproblematic for TCP with retransmission control.
Evaluating Against International Standards
Let us compare the numbers so far against the international standard ITU-T Y.1541, which defines quality objectives for IP networks.
Y.1541 is a recommendation that measures the quality of an IP network across three dimensions—delay, delay variation, and loss rate—and defines "QoS classes" as upper limits that must be met depending on the application. It serves as a common language when carriers commit to network quality, with values defined as follows.
| Parameter | Class 0 / 1 | Class 2 / 3 | Class 4 | Class 5 |
|---|---|---|---|---|
| IPTD (IP Packet Transfer Delay) | 100 ms / 400 ms | 100 ms / 400 ms | 1 s | Not specified |
| IPDV (IP Packet Delay Variation: difference between 99.9th percentile and minimum of one-way delay) | 50 ms | Not specified | Not specified | Not specified |
| IPLR (IP Packet Loss Ratio) | 10⁻³ | 10⁻³ | 10⁻³ | Not specified |
| Assumed use | Real-time communications sensitive to jitter such as VoIP and video conferencing | Transactions, signaling | Video streaming, bulk transfer |
Evaluation is performed per minute, and every observed minute must satisfy the criteria. Delay variation is defined separately for uplink and downlink. Since irtt records one-way delay, uplink and downlink can be evaluated separately.
The results of evaluating the 60 minutes of data one minute at a time are as follows.
| Parameter | Measured value | Judgment |
|---|---|---|
| IPTD | Average 15.5 ms (approximated as half of RTT). Even in the worst 1-minute window, 18.3 ms |
Well below Class 0's 100 ms |
| IPDV (downlink) | Median 13.7 ms, maximum 50.8 ms. Exceeded 50 ms in 1 out of 60 minutes |
Almost satisfies Class 0 / 1's 50 ms |
| IPDV (uplink) | Median 64.1 ms, maximum 135 ms. Exceeded 50 ms in all 60 out of 60 minutes |
Does not satisfy Class 0 / 1 |
| IPLR | 8.1×10⁻⁴ over the full 60 minutes. On a per-minute basis, 14 out of 60 minutes exceeded 10⁻³ |
Satisfies Class 0–4 overall, but does not satisfy the per-minute condition |
The absolute value of latency is Class 0 equivalent. One-way latency around 15 ms presents no significant issues compared to terrestrial fixed-line connections, and it is fair to say this is in a different category from geostationary satellites with one-way latency exceeding 250 ms.
On the other hand, the reason it falls short of the highest Class 0 / 1 is delay variation, particularly uplink delay variation. Downlink variation is 14 ms, within the Class 0 threshold, but uplink is 64 ms, 1.3 times the standard. Combined with the earlier finding that loss was skewed toward the uplink, it appears that the primary direction affected by handovers is from the terminal to the satellite.
Loss rate is borderline. The hourly average is within the standard, but since loss concentrates at handover boundaries, one in four one-minute windows exceeds the standard when evaluated per minute.
In summary, under no-load conditions, Starlink Mini reliably satisfies Class 2–4 criteria (transactions, video streaming, bulk transfer), but does not satisfy Class 0 / 1 (VoIP, video conferencing) due to uplink delay variation. This does not mean VoIP cannot be used; rather, since the network cannot guarantee keeping variation within 50 ms as Y.1541 assumes, the design must absorb this on the terminal side using a jitter buffer.
One caveat applies to this evaluation. Y.1541 requires 1,000 or more packets per minute to evaluate the 99.9th percentile, but this measurement used approximately 600 packets at 100 ms intervals, making the per-minute loss rate judgment particularly coarse. To evaluate in strict accordance with the standard, re-measurement at intervals of 50 ms or less would be required. Also, Y.1541 is originally intended to evaluate carrier network quality between UNIs, so the premise differs slightly from a measurement like this one that includes host-internal processing on the Mac and EC2.
Does This Affect Manufacturing Floor Design?
Applying the results of this measurement to the design of connecting floor equipment to the cloud, I think the following applies. Note that the design guidelines below are rough estimates based on no-load measurements. Since jitter and loss rates may change under actual workload conditions, load testing in the production configuration is recommended.
| Protocol / Use case | Impact of 15-second cycle | Design approach |
|---|---|---|
| TCP in general (HTTPS, MQTT over TCP) | Outages of 200 ms or less are absorbed by retransmission. Throughput drops slightly due to heavier RTT | Do not set keepalive extremely short. Do not treat retransmissions of a few hundred ms as "abnormal" |
| MQTT keepalive / session | Since no outage exceeded 1 second, disconnection should not occur if keepalive is set to tens of seconds | Addressable with QoS 1 and persistent sessions |
| Streaming / UDP-based (video, audio, some industrial protocols) | Jitter of tens to over a hundred ms and loss of 100–200 ms every 15 seconds | Allow a jitter buffer of 200 ms or more. Use encoding and forward error correction that assume loss |
| Uplink-heavy sensor data | Loss is skewed toward the uplink | Place buffering and retransmission on the sending side (the floor) |
Caveats
- These are measurement results using a Starlink Mini at a location in Yokohama, Kanagawa Prefecture. Results may differ depending on the measurement location and antenna.
- These results are from a single location, a single session, during 60 no-load minutes in the daytime. Results may differ during nighttime peak hours or in different weather conditions.
- Measurements were taken at a location with no obstructions. If obstructions are present, disconnections caused by obstructions would be mixed in, making it impossible to distinguish from handover-related outages.
- The Starlink terminal's gRPC API is unofficial. Field names and behavior may change with firmware updates.
- Behavior when bandwidth is saturated with iperf3 is outside the scope of this article. A separate article is planned.
Closing
I have recently been interested in Amazon Leo and have been researching information on satellite communications. I expected satellite handovers to affect communication quality, but I was surprised to read in the paper that they occur as frequently as every 15 seconds. When I actually measured it, the 15-second cycle effect was clearly visible.
At the same time, the impact is at most 200 ms of loss, with RTT at p99 around 100 ms, yet a loss rate of 0.08% and an average latency of around 30 ms make this a sufficiently high-quality network connection. For TCP-based protocols, it is at a level that can be used without conscious awareness, and I feel that LEO satellite communications are fully practical as a connectivity option for manufacturing floors. For UDP-based protocols sensitive to jitter and uplink sensor data flows, designing with the figures from this measurement in mind would be prudent.
Next, I am thinking of writing an article summarizing the equipment needed to actually operate a Starlink Mini outdoors.