Predicting Where a Radiosonde Will Land — and Testing It Against a Real Flight
Twice a day, every day, the Royal Meteorological Institute launches a weather balloon from Uccle, just south of Brussels. Hanging under it is a small styrofoam box — a radiosonde — that climbs to around 30 km, bursts, and parachutes back down somewhere downwind. The balloons aren’t recovered, so the sondes become litter, and a small community of hobbyists tracks them on radio and goes out to pick them up.
To pick one up, you need to know where it’s going to land. That turned into a small obsession of mine: can I predict the landing spot from nothing but the sonde’s own radio telemetry — no weather model, no internet? And, just as importantly, how would I ever know if my prediction was any good?
This post is the answer to both: the predictor I wrote, and the day I caught a real flight with a cheap ESP32 board and checked my prediction against where the sonde actually came down.
What a radiosonde tells you#
The sondes around here are Vaisala RS41s. They transmit on the 400–406 MHz band (Uccle uses 403.5 MHz) with a simple GFSK signal, roughly one frame per second. Each frame carries a GPS fix — latitude, longitude, altitude — plus pressure, temperature and humidity.
That once-per-second stream of GPS positions is everything my predictor gets to work with. No wind forecast, no atmospheric model downloaded from anywhere — just the trail of where the sonde has been over the last minute.
The existing tools do it differently. The excellent radiosonde_auto_rx and SondeHub predict landings too, but they lean on GFS wind forecasts. That’s more accurate, but it needs internet and a weather model. I wanted something completely self-contained — the kind of thing that keeps working in a car in the middle of nowhere with no signal.
The physics: two problems that don’t talk to each other#
A descending sonde is really two independent motions: it falls, and it drifts. Handily, you can model them separately.
Falling: terminal velocity and thin air#
Under its parachute the sonde is at terminal velocity — drag balances gravity. The trick is that drag depends on air density, and the air up high is far thinner than at the ground. So the sonde falls fast near burst altitude (tens of metres per second) and slows to a gentle few m/s as it nears the ground.
For a parachute, terminal velocity scales as 1 / sqrt(density). That means the
quantity v · sqrt(density) stays roughly constant through the whole descent. So
I don’t need to know the sonde’s mass or chute area — I can pin that constant
straight from the descent rate the sonde is already showing me:
// Calibrate the fall from the sonde's own recent descent rate (m/s),
// at the current altitude's air density:
double k = fabs(observed_descent_rate) * sqrt(isa_density(current_alt));
// Then fall to the ground one second at a time, letting the speed
// change as the air thickens on the way down:
double alt = current_alt;
int seconds = 0;
while (alt > ground_elevation && seconds < 600) {
double rho = isa_density(alt);
alt -= k / sqrt(rho); // this second's descent step
seconds++;
}isa_density() is the International Standard Atmosphere — a well-known
piecewise model of how pressure, temperature and density vary with height. It’s
a handful of atmospheric layers, each with its own temperature lapse rate, glued
together with the barometric formula:
// (one layer of the ISA model — it has seven, up to ~85 km)
if (lapse_rate == 0.0) {
T = base_temp;
P = base_press * exp(-g0 * Mair * dH / (Rstar * base_temp));
} else {
T = base_temp + lapse_rate * dH;
P = base_press * pow(base_temp / T, g0 * Mair / (Rstar * lapse_rate));
}
return P * Mair / (Rstar * T); // density = P·M / R·T
When that loop exits, seconds is my estimate of how long the sonde has left in
the air.
Drifting: just carry the wind#
While it falls, the sonde drifts with the wind. I don’t try to be clever here. I fit the recent latitude and longitude as straight lines in time over the last minute or so, and read them off at the predicted landing time:
// Straight-line fit of lat & lon vs. time over the recent history,
// extrapolated forward to the moment it hits the ground:
LinearFit lat_fit = fit_linear(t, lat_samples, n);
LinearFit lon_fit = fit_linear(t, lon_samples, n);
double land_lat = extrapolate(&lat_fit, seconds);
double land_lon = extrapolate(&lon_fit, seconds);That simple linear “carry the current drift forward” is deliberate. My first attempt fitted a quadratic to the track, on the theory that it would capture the curve as the wind changes with height. It backfired: over a long extrapolation the squared term runs away and throws the landing point kilometres off. A boring straight line won.
The catch: it’s only any good near the ground#
There’s an honest limitation baked into all of this. High up, the sonde has a
long way still to fall, and any small error in the drift estimate gets multiplied
over all that time. The prediction only becomes trustworthy in the last few
kilometres — the final ten minutes or so of the flight. That cap is literally
the seconds < 600 in the loop above: if the model can’t reach the ground inside
600 seconds, I don’t trust the answer yet.
Catching a real one#
A predictor is easy to fool into looking good on paper. The only test that counts is a real flight. So I set up to receive one.
The receiver is a LilyGo TTGO T-Beam — an ESP32 with a Semtech LoRa radio, about €30 — running the open rdz_ttgo_sonde firmware, which decodes RS41s beautifully. A modest antenna at home was enough.
For ground truth I leaned on a second, independent receiver: an RTL-SDR dongle
running radiosonde_auto_rx, which uploads every fix to SondeHub. That gives me
two things — a sanity check that my T-Beam is decoding correctly, and an
authoritative record of where the sonde actually ended up.
I caught the midday Uccle flight, serial W4615045, and logged the whole thing at one sample per second from start to finish: the climb, the burst at almost 34 km, and the long ride back down.

First, the sanity check. My little T-Beam’s decoded positions matched the reference receiver exactly at every altitude — and, pleasingly, it actually held the signal lower on the final descent than the reference station did, following the sonde down to 214 m where the other receiver lost it at 292 m. The cheap board punched above its weight.
The moment of truth#
Now the part that matters. My method was simple and, I think, the most honest way to test a predictor: capture the whole flight, then replay the descent through the predictor. I take the logged descent, pick a moment — say, when the sonde is 500 m up — feed the predictor only the history it would have had at that instant, and ask it: where does it land? Then I compare that to where the sonde actually touched down.
For W4615045, predicting from 500 m up, the predictor put the landing 50 metres from the real touchdown. Fifty metres, from a model that knew nothing about the weather — only the sonde’s own last minute of GPS.
Here’s how the error grows the higher up you ask, and how the simple linear carry compares to that ill-fated quadratic:
| Predicting from… | Telemetry-only (linear) | Quadratic fit |
|---|---|---|
| 1000 m up | 293 m | 136 m |
| 600 m up | 56 m | 208 m |
| 500 m up | 50 m | 154 m |
| 400 m up | 51 m | 85 m |
Two things jump out. First, the prediction is excellent precisely where you need it — in the last few hundred metres, where you’re deciding which field to walk into — and gets vague higher up, exactly as the physics warned. Second, in that final stretch the boring straight line beats the clever quadratic comfortably. (The quadratic’s apparent win at 1000 m is moot: neither answer is worth acting on from that high.)

Does it actually hold up? Backtesting against years of flights#
Fifty metres on one flight is a nice headline, but it’s also just an anecdote. A predictor that nails a single calm descent could still be useless on a gusty one. The only way to actually trust it is to run it against a whole back-catalogue of real flights — a backtest.
I had three sources of historical truth to draw on:
SondeHub open data. Every sonde the community has ever tracked is archived, full flight, free to use (CC0). A small script walks a list of serials and pulls each flight’s export, caching just the descent from burst to landing:
# SondeHub archives every sonde's full flight by serial url = f"https://api.v2.sondehub.org/sonde/{serial}"My own
auto_rxstation logs — the flights I’d personally received off the air, rather than someone else’s receiver.And the best ground truth of all: my recovery logbook. I’ve been chasing and picking up these sondes for years, with a folder of dated, serial-stamped recoveries going back to 2023. For those, I don’t just have the telemetry — I know exactly where each one ended up, because I stood there and picked it up.
The backtest is the same trick as before, run at scale. For each flight I wind the clock back to 500 m up, hand the predictor only the history it would have had at that moment, and compare its answer to where that sonde really came down.
Across 54 flights the picture was clear and honest:
| How close did it get? | Telemetry-only predictor |
|---|---|
| Within 250 m | 37% of flights (20 / 54) |
| Within 500 m | 76% (41 / 54) |
| Within 1 km | 91% (49 / 54) |
| Median miss | 294 m |
| 90th percentile | 721 m |
| Worst case | 2.3 km |
So the 50 m on W4615045 was a good day. The typical day is a few hundred metres — three flights in four land within 500 m of the prediction, easily close enough to pick a field and walk in — and a long tail of harder cases stretches out to a couple of kilometres.

What makes a flight hard?#
The tail isn’t random. The error is correlated with how long the descent takes (about +0.4): the longer the sonde dawdles under its parachute, the more time the wind has to do something my straight-line carry didn’t see coming. Split the corpus at a 150-second fall from 500 m and it’s stark:
- Brisk descents (41 flights): median miss 264 m
- Slow, drawn-out descents (13 flights): median miss 568 m — more than double
That makes physical sense. A fast descent is over in a minute and the wind barely changes; a slow one drifts for several minutes through air that may be moving quite differently at 400 m than at 100 m. The best flight in the set missed by just 52 m; the worst, a slow one, by 2.3 km.
Linear vs quadratic, settled#
This corpus is also where the linear-versus-quadratic question got settled for good, and it wasn’t close. You can see it in the chart above — the grey (quadratic) curve tracks the blue one in the middle but falls away badly in the tail. The quadratic fit’s median is only a little worse (358 m vs 294 m), but its 90th-percentile error is roughly twice as bad (≈1.5 km vs ≈0.7 km): on the exact slow, windy flights where you most need a sane answer, the squared term runs away and throws the landing kilometres off. The straight line wins on 31 of 54 flights, and — more importantly — it almost never fails badly. One lucky flight will never tell you that. A backtest will.
Lessons and honest caveats#
I’m pleased, but I’m not going to oversell it, and the backtest’s tail is exactly where the honesty lives. Carrying the wind forward as a straight line has no idea about wind shear — the way the wind direction swings as you drop through different layers — so the fast, gusty descents are precisely the ones it handles worst. That’s where a model-based predictor like SondeHub’s, with real wind data, will pull ahead.
But for a self-contained, no-internet predictor that fits in a few hundred lines of C and runs on the same little ESP32 that’s doing the receiving, a typical miss of a few hundred metres — and a stone’s throw on a good day — is more than enough to point a car at the right field. Next on the list: folding in a crude wind estimate to soften that shear problem, and feeding every new chase back into the backtest so the error picture keeps sharpening.
If you’ve ever watched a dot drift across a map and wondered where it’ll come to rest — it turns out the sonde has already told you, if you listen to the last minute of its fall.
Resources#
- rdz_ttgo_sonde firmware — the RS41 decoder I run on the T-Beam
- radiosonde_auto_rx — the RTL-SDR receiver I used as ground truth
- SondeHub — live radiosonde tracking and the authoritative landing record
- International Standard Atmosphere
— the atmospheric model behind
isa_density() - Vaisala RS41 — the sonde itself