PX4's state estimator, EKF2, has to know how old each GPS sample is when it arrives. Up to PX4 v1.17 that is the parameter EKF2_GPS_DELAY, 110 ms by default. Almost nobody changes it: in the public flight logs we read, 96-98% of vehicles ran the default.
So we measured it. We took random samples of real flights uploaded to PX4 Flight Review since October 2025, one log per vehicle, and wrote down every prediction before downloading anything.
How the delay is measured
gps_lag.py compares two things that should tell the same story. One is the change in GPS velocity between consecutive GPS samples. The other is the IMU's acceleration, rotated to north-east and integrated over the same interval, shifted back by a candidate lag. The lag where they fit best is the GPS delay. It reads no position, so no location leaves the log. Hover-only flights don't move enough to measure, so they don't pass its gates.
What the fleet says
- First sample: 164 logs from 115 vehicles. Median lag 99.8 ms, close to the default. But 29.6% of logs were more than 50 ms off their own setting. Two logs from the same vehicle agreed to a median 5.3 ms, so this is a property of the vehicle, not noise.
- Replication: 98 logs from 98 other vehicles. Median 113.8 ms; 31.2% more than 50 ms off. Both figures fell inside ranges written down before the run.
Does EKF2 agree?
A measured lag is only useful if it means something to the filter. EKF2 logs its GPS velocity innovations, the difference between what it predicted and what the GPS said. If the GPS is later than the filter thinks, the innovation should lean in a predictable direction whenever the vehicle accelerates.
We pre-registered that check, and the first run failed as written: strong agreement, but with the opposite sign. The plan had assumed the textbook innovation sign, measurement minus prediction. PX4 computes prediction minus measurement (_state.vel - velocity in gps_control.cpp). We didn't regrade after seeing the data. We applied the written rule, withdrew our suggested setting, published why, and pre-registered the corrected check on the 98 fresh logs, whose innovations nobody had read.
It held. On 89 validated logs:
- the two methods rank the logs alike (Spearman +0.86);
- in 25 of 27 logs more than 50 ms off their setting, EKF2's residuals lean the same way.
So the tool again offers the measured lag as a starting value for the delay setting when it is more than 50 ms off. Check it with EKF2 log replay before flying it. Agreement in direction and rank is not a calibration.
Does it matter to the filter?
EKF2 accepts or rejects each GPS velocity sample with an innovation test: a test ratio above 1 means rejected. We pre-registered whether a wrong delay shows in that ratio, on a third, fresh sample of 256 logs (mismatch-2026).
- It shows. While the vehicle accelerated, the ratio was a median 2.7 times its steady-flight value on logs more than 50 ms off their setting, against 1.7 times on the others. The larger the mismatch, the larger the rise.
- Rejections: 20% of mismatched logs had at least one GPS velocity sample rejected in flight, against 16% of the others. That gap is within chance, but the interval reaches +14 points, so our own "no extra rejections" check failed. We can't say a wrong delay is harmless.
Does the receiver model matter?
PX4 records which GPS receiver it detected in every GPS message's device id. ArduPilot's u-blox driver gives M8, M9 and F9 the same 120 ms, with the comment that the F9 figure is "not verified yet from flight log".
On three sets of PX4 vehicles with none in common, the u-blox M9 came out far less late than the F9P and the M8 (median lag, number of vehicles):
| M9 | F9P | M8 | |
|---|---|---|---|
| first receiver sample (163 logs) | 71 ms (25) | 143 ms (16) | 147 ms (15) |
| earlier samples, receiver read afterwards (213 logs) | 76 ms (32) | 144 ms (27) | 151 ms (7) |
| fresh sample, November (298 logs) | 81 ms (29) | 143 ms (42) | 145 ms (12) |
M9 − F9P was −67 ms (95% interval −101 to −48) and then −62 ms (−88 to −42). The F9P and the M8 were within 8 ms of each other every time, as ArduPilot assumes. But every study's own enough-data check failed, each time on the M8 (12 turned up in the fresh sample against 15 needed), so by the rules we wrote in advance this is still exploratory. And a default per receiver model would barely help: the spread within each class is wide, so it would put only 2.3 points more vehicles within 50 ms of their real lag. Knowing the model doesn't replace measuring your own vehicle.
It isn't the measurement rate. We read each log's actual rate from the receiver's own time stamps (GRADE-rate.md, pre-registered). On the same 125 ms grid, M9 logs were still 53 ms less late than the other u-blox logs (interval −106 to −19 ms). One of our own predictions failed, and the reason is worth knowing if you fly an F9P: since PX4 v1.16, the driver asks an M9 for 10 Hz and an F9P L1L2 for 5 Hz (PX4-GPSDrivers bbdd576, January 2025). Before that it was 8 Hz and 10 Hz. Our plan had read the older driver.
EKF2's GNSS innovations on real flights
PX4 RFC #28837 argues, from a simulation, that EKF2 treats every GNSS sample as independent although real GNSS errors stay correlated, which leaves the filter's normalised innovations tiny and its gate far too wide. We measured that on 285 real flights from the same fresh sample (nis-2026, pre-registered):
- the typical log's position NIS was 0.0079, where a consistent filter gives 0.693; velocity 0.024;
- in units of the innovations' real spread, the 5-sigma gate is about 30 sigma wide for position and 17 for velocity (the RFC's simulation: more than 11);
- position innovations stay correlated for a few seconds (0.78 at 1 s, 1/e after about 3.7 s); velocity ones fade within about a second.
These are EKF2's innovations, not raw GNSS errors, and they are logged every 500 ms, so the limits are in the grade.
If you fly PX4 v1.18: the delay is now per receiver
PX4 v1.18 removes EKF2_GPS_DELAY (PX4 PR #26660). Each receiver now gets its own delay, SENS_GPS0_DELAY and SENS_GPS1_DELAY (110 ms by default, matched to a receiver through SENS_GPS0_ID / SENS_GPS1_ID). The sensors module moves each sample's timestamp_sample back by that much, and EKF2 fuses the sample at that time. Old values are migrated on upgrade. On PX4 main the parameters are already renamed SENS_GNSS0_DELAY and SENS_GNSS1_DELAY, and vehicle_gps_position becomes vehicle_gnss (PR #24399).
The fresh sample had 8 logs from v1.18 pre-releases. None has EKF2_GPS_DELAY, and in all 8 the sample times were moved back exactly 110.0 ms. On one of them, with an NMEA receiver, the measured lag was 187 ms, 77 ms later than the delay applied.
The PR's motivation is multi-receiver setups. Our numbers suggest the receiver model alone matters too. A vehicle with an M9 and an F9P may want two quite different values, and a per-receiver parameter is now where they go. Measure your own vehicle rather than copying our medians.
While we were in there: flight health
px4-log-health reads the same logs for the things Flight Review shows by eye: vibration, accelerometer clipping, GPS jamming and spoofing, battery warnings, failsafes and motor saturation. On 208 public flights from 208 vehicles, 75% raised at least one flag.
One finding surprised us. Flight Review colours the vibration plot green, orange and red at 4.9 and 9.8 m/s². But PX4 v1.12 and older computed that metric from delta velocities, in m/s, about 200 times smaller. On those logs the bands don't mean anything, and old logs look quiet when they aren't. Among v1.13+ logs, 51% sit in orange or red. Version 0.2.0 reads the release from the log and doesn't band old ones. We've described this in Flight Review issue #352.
One report per flight: in your browser, on the command line or in CI
checkup.py puts both together: one short Markdown report per log with the health flags, every figure placed among those 208 public flights, and a GPS delay verdict that measures the lag, compares it with the log's delay setting and checks the direction against EKF2's innovations. It suggests a value only when the lag is more than 50 ms off and the innovations don't disagree. Since version 0.3.2 it also reads PX4 v1.18 logs: it takes the delay EKF2 applied from the log's own sample times and names the SENS_GPSn_DELAY to change. Here is a real report on three public logs.
In your browser, with nothing uploaded: munimv2.github.io/px4-log-check runs checkup.py in Pyodide on your own computer (version 0.3.1 for now). Pick a .ulg and the report appears. On the three sample logs it gave a report identical to the command line's.
If you keep test-flight logs in a repository, the same report can land in every CI run's job summary:
- uses: actions/checkout@v4
- uses: munimtechnologies/px4-log-health@v0.3.2
with:
logs: "**/*.ulg"
Or on the command line:
pip install numpy pyulog requests
git clone https://github.com/munimtechnologies/px4-log-health && cd px4-log-health
python fetch_deps.py # gps_lag.py and innov.py from px4-gps-lag, pinned commit
python checkup.py flight.ulg # or: python health.py flight.ulg / python gps_lag.py flight.ulg
Everything is open: the plans written before each run, the grades, and one line per log (vehicle ids hashed, no positions).
- GPS delay grades: GRADE-innov-confirm.md, GRADE-innov.md (the sign mistake), GRADE.md, mismatch-2026
- Receiver model: GRADE.md, GRADE-confirm.md, GRADE-rate.md, GRADE-fresh.md
- EKF2 innovations: nis-2026
- Fleet health: GRADE.md
Limits: these are uploaded logs, often uploaded because something went wrong, so they are not the rate in the air. The model is first-order, and both methods share the attitude estimate. Receiver classes are as PX4 detected them; the M8 class is small.
Data: PX4 Flight Review public logs (Dronecode / PX4, CC BY 4.0). Code Apache-2.0; text CC BY 4.0.
Disclosure: the analysis, the code and this post were made with AI agents working in the name of MV2 of Munim, Inc.
Top comments (0)