You open Flightradar24, FlightAware, and your own API response side by side for the same flight. FR24 puts it at 51.2741, -0.6759 at 9,075 feet. FlightAware has it a couple of miles further along the track. Your API says 8,950 feet. Nothing matches, and the obvious conclusion is that somebody is wrong.

Usually nobody is. Most of these disagreements come from three sources being honest about slightly different things: a different observation, made at a different moment, processed under different assumptions. The differences are explainable and mostly predictable once you know what to look for, and the ones that remain unexplained are the ones actually worth escalating to a vendor.

This post is the explanation. It's the kind of thing data vendors avoid writing because it means conceding that no feed is perfect, including ours.

The comparison most people run isn't a fair one

Before the technical causes, the methodology problem. The usual comparison is: look at a map in a browser, look at a JSON response in a terminal, note that they differ.

That comparison is invalid, because a map is not showing you an observation. It's showing you an animation. Consumer flight trackers interpolate between received positions and extrapolate forward from the most recent one so the aircraft glides across the screen instead of hopping every few seconds. The dot you're looking at may be a dead-reckoned estimate computed from a position that arrived eight seconds ago and a velocity vector from the message before that.

An API response, by contrast, hands you the last thing that was actually received, with a timestamp attached. In our case that's last_seen, alongside a timestamp for when the snapshot itself was generated:

{
  "icao24": "0A0022",
  "latitude": 51.274109,
  "longitude": -0.675964,
  "altitude": 9075.0,
  "ground_speed": 298.3,
  "track": 153.09,
  "last_seen": "2026-06-16T14:21:45.674566"
}

So you're comparing a rendered projection against a raw observation. At 300 knots an aircraft covers about 0.083 nautical miles per second, so a 10-second difference in what "now" means puts the two representations 0.8 nm apart before any real data quality question enters the picture. At 480 knots in cruise it's 1.3 nm. Neither source is wrong; they're answering different questions.

The fix is to compare observations to observations. Use each source's own timestamp field, and only compare positions whose timestamps are close. Everything below assumes you're doing that.

A split-flap departures board listing flight numbers, destinations, and departure times

Where the disagreement actually comes from

Different receivers heard it

ADS-B is a broadcast. There is no authoritative copy — there's whatever your receivers happened to pick up. Every tracking service runs its own network of ground stations, plus whatever feeds it buys or trades for, and those networks have different coverage.

The practical consequence: over a well-covered area like the Benelux or the US Northeast, two networks will both have near-continuous reception and their positions will agree closely, because they're both decoding the same broadcasts. Over the mid-Atlantic, central Africa, or Siberia, one may have satellite-based reception and the other may have nothing at all, or may be filling from a much slower source. Discrepancies cluster geographically, and if you sample only over Europe you'll conclude every vendor is equivalent.

One source has MLAT and the other has ADS-B

Aircraft without ADS-B Out still reply to Mode S interrogations with identity and altitude but no position. Multilateration recovers a position from the nanosecond-scale differences in when four or more receivers hear the same reply.

MLAT works, but it isn't equivalent to ADS-B. Accuracy depends on receiver geometry — the positions are good when receivers surround the aircraft and degrade badly when they're clustered on one side. Errors of a few hundred metres are normal and multiple kilometres happen at the edges of a receiver cluster. Update rates are lower and more irregular.

If FR24 has an MLAT solution for an aircraft and you have nothing, the aircraft is simply missing from your feed rather than misplaced. If both have MLAT solutions computed from different receiver sets, expect them to differ by more than ADS-B positions ever would. Neither is a bug. There's more on how these sources get blended in how Flightradar24 actually works.

"Altitude" means at least three different things

This is the one that catches people most often, because the number is right there in both responses and it's off by several hundred feet.

The airborne position message broadcasts pressure altitude referenced to the standard datum, 1013.25 hPa (29.92 inHg). Not height above sea level. Not height above ground. Pressure altitude on a fixed reference pressure that has nothing to do with today's weather.

Above the transition altitude that's exactly what everyone wants, because all traffic is separated on the same standard datum — that's what a flight level is. Below it, aircraft fly on local QNH while the transponder keeps reporting on the standard datum. Roughly 1 hPa of difference is 27 feet, so on a day with a 985 hPa QNH the broadcast pressure altitude sits about 760 feet below the aircraft's actual height above sea level.

Now the divergence. Some services display the raw broadcast value. Some correct it to MSL using the QNH from the nearest station. Some show geometric altitude when it's available, since aircraft also broadcast GNSS height in a separate message type, and the velocity message carries the difference between the two. Three defensible choices, three different numbers for one aircraft.

Our altitude field is barometric, in feet. When you compare against another source, check what theirs is before concluding anything. A consistent few-hundred-foot offset at low altitude that vanishes above FL180 is a datum difference, not an error.

Fusion picked a different winner

In busy airspace a single aircraft is heard by dozens of receivers, each producing its own copy of the same message, arriving at the aggregator at slightly different times with different reception quality. Somebody has to decide which one becomes "the" position.

The strategies differ. Take the most recent. Take the one with the best reported navigational accuracy. Take the one from the nearest receiver. Weight several together. Prefer ADS-B over MLAT over radar-derived when more than one is available for the same aircraft, with different thresholds for switching between them.

These are all reasonable, and they produce different outputs from identical inputs. Fusion policy is also where a source's character shows: a system tuned to prefer the freshest message is more responsive and noisier, one tuned to prefer high-quality messages is smoother and lags slightly.

The non-position data comes from a completely different pipeline

Positions are one thing. Status, gates, and times are another, and they're where the largest disagreements live.

"Departed," "estimated arrival 14:32," "gate B14" — none of that is broadcast by the aircraft. It comes from airline feeds, airport systems, regulatory data sources, and in some cases inference from the position data itself. Two services can hold identical position data and still disagree on whether a flight has departed, because one is reading an airline's status message and the other is inferring wheels-up from ADS-B ground state.

The inferred version is often faster and occasionally wrong. The reported version is authoritative and often stale. When a flight shows as "on time" on one site and "delayed" on another, this is almost always why, and it has nothing to do with position accuracy.

Somebody is honouring a filter

A number of aircraft are deliberately suppressed or anonymised through programmes like LADD and PIA. Services apply these filters differently, and some apply additional filtering of their own. An aircraft that's absent from one source and present in another may be a blocking policy difference rather than coverage.

A Boeing 777 in cruise above broken cloud over mountainous terrain

Comparing sources properly

If you're evaluating vendors or debugging a discrepancy, the comparison needs to control for the things above. The shape that works:

Sample many aircraft, not one. A single flight tells you nothing — you need a distribution. Pull a few hundred aircraft across several regions, including at least one with weak coverage.

Match on icao24, never on callsign. Callsigns change mid-flight, get retyped by crews, and are formatted differently by different vendors.

Compare positions only when both observations are recent, and account for the age difference that remains. The useful metric isn't raw separation — it's separation after correcting for how far the aircraft would have travelled in the time between the two observations.

import requests
from datetime import datetime, timezone
from math import radians, sin, cos, asin, sqrt

BASE = "https://skylink-api.p.rapidapi.com"
H = {"X-RapidAPI-Key": KEY, "X-RapidAPI-Host": "skylink-api.p.rapidapi.com"}

def haversine_nm(lat1, lon1, lat2, lon2):
    lat1, lon1, lat2, lon2 = map(radians, (lat1, lon1, lat2, lon2))
    h = sin((lat2 - lat1) / 2) ** 2 + cos(lat1) * cos(lat2) * sin((lon2 - lon1) / 2) ** 2
    return 2 * 3440.065 * asin(sqrt(h))

r = requests.get(f"{BASE}/adsb/aircraft", headers=H,
                 params={"bbox": "51.2,-0.8,51.6,-0.2"})
payload = r.json()
snapshot_at = datetime.fromisoformat(payload["timestamp"]).replace(tzinfo=timezone.utc)

for a in payload["aircraft"]:
    if a.get("latitude") is None or a.get("ground_speed") is None:
        continue
    seen_at = datetime.fromisoformat(a["last_seen"]).replace(tzinfo=timezone.utc)
    age_s = (snapshot_at - seen_at).total_seconds()

    # How far this aircraft could have moved since it was last observed.
    # Any comparison against another source has to live inside this envelope.
    drift_nm = a["ground_speed"] / 3600.0 * age_s
    print(f"{a['icao24']} {a.get('callsign') or '':8} "
          f"age={age_s:5.1f}s  uncertainty={drift_nm:.2f} nm")

Run that and you'll see the honest error bar on any single position. An aircraft last heard 25 seconds ago at 450 knots carries about 3.1 nm of positional uncertainty regardless of how precise the stored coordinates look. Comparing two sources without accounting for this produces numbers that say more about polling timing than about data quality.

The flight data accuracy benchmarking guide covers the full methodology, including coverage sampling and latency percentiles.

When it is actually a bug

Having explained away most discrepancies, some are real. The ones worth reporting to any vendor, us included:

A position that is hundreds of miles off rather than a few miles. Small offsets are timing and fusion; a large one is usually a decoding failure, and there's a specific mechanism behind it — position decoding pairs two messages against a grid, and a bad pair resolves to a location roughly 360 nm away in latitude. The ADS-B message anatomy post explains why that number is what it is.

Altitudes that are wrong by an exact multiple of 25 feet in a way that tracks with nothing, or that flip between plausible and absurd on consecutive updates. That's a decoder problem, not a datum problem.

An aircraft that stays in the feed with a frozen position while its timestamp keeps advancing. That means something is republishing a stale record as though it were fresh, which is worse than dropping it.

Systematic absence of traffic in a region where a competitor has continuous coverage, sampled over hours rather than once.

Everything else on the list above is the physics and the plumbing, and it will still be there after any vendor switch.

What to do about it

If you're building on live position data, the practical stance is to treat position as an observation with an age rather than a fact. Read last_seen on every record. Decide your own staleness threshold and drop or grey out anything past it. If you want smooth movement on a map, do the interpolation client-side where you control the assumptions and can stop projecting when the data gets old, rather than receiving pre-smoothed data you can't unpick.

The next problem after this one is what to do when the data is not merely late but wrong — jumps, altitude spikes, and coverage dropouts. That's covered in handling gaps, jumps, and bad positions.

The ADS-B endpoint returns observations with timestamps rather than projections, which is the shape you want if you're doing any of this analysis yourself. The free trial is enough to run a real comparison against whatever you're using today — and if the numbers come out against us somewhere, we'd genuinely rather hear about it than not.