I actually tried to observe satellite handover on Starlink Mini

I actually tried to observe satellite handover on Starlink Mini

I measured the communication status with Starlink Mini and actually confirmed the impact on communication caused by satellite switching at 15-second intervals.
2026.09.15

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.

PXL_20260913_064253184

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

PXL_20260913_044009794

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 -i to prevent the Mac from sleeping
  • Since macOS ping lacks Linux's -D flag (epoch display), use --apple-time and TZ=UTC to 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

2026-09-14-starlink-test-1min

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.

2026-09-14-starlink-idle-day-timeseries

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.

2026-09-14-starlink-idle-day-phase-hist

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).

2026-09-14-starlink-idle-day-zoom-worst

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.


製造業のクラウド活用とデジタル化を支援します

クラスメソッドの専門家による包括的なクラウド導入とデジタル化支援で、製造業の業務効率を最大化しましょう。AWSの導入から運用、最適化まで、最新技術と豊富な知見であらゆる課題に対応します。生産ラインのデジタル化やデータ活用、IoTの導入事例もございます。ぜひ、弊社の実績をご覧ください。

製造業界での支援内容を見る

Share this article