How mlat.uk works.
- SDR receivers
- Multilateration
- Data engine
- Live API
- One engineer
A volunteer network of software-defined radios, a Rust multilateration server, a purpose-built data engine and a bespoke streaming API, fused into one live picture of air, maritime and space traffic. Every layer, from the antenna to the pixels, was designed, written and is operated by one person. This page follows a single radio signal from the antenna to your screen.
Three domains in, one picture out.
Air, maritime and space data arrive by completely different routes. They are kept apart until the data engine, where they are normalised into one object model and published as a single stream.
Swipe sideways to see the whole diagram.
- Air
- Maritime
- Space
- Fused stream
- Batch, history and reference data
- Receive. Antenna, filter and LNA feed the SDR, which digitises 1090 MHz. The decoder software (readsb) demodulates and decodes Mode S, stamping each frame with a 12 MHz timestamp.
- Beast feed. Deduplicated Beast frames go to
feed.mlat.uk:30004and into the central readsb aggregator. - MLAT feed. The same receiver sends timestamped frames to mlat-server-rs on
31090, and the server returns each receiver’s sync state and results over the same connection. - Multilaterated positions go back into readsb as Basestation (SBS) lines, so ADS-B and MLAT share one track per aircraft.
- Aircraft stream. readsb pushes only the aircraft that changed to the aircraft worker.
- AIS. The same RF chain on 162 MHz feeds AIS decoder software (AIS-Catcher or Shipfeeder), which sends NMEA over UDP to the maritime ingest.
- AISHub. Vessel data exchanged between contributors is pulled in; local receivers take priority.
- CelesTrak. Orbital elements and the SATCAT catalogue are loaded, grouped and tiered.
- Reference data. Airports, weather, hazards, the aircraft database and routes are refreshed and cached by the airdata and routecache workers.
- Live state. Each worker publishes trimmed, delta-only updates to Redis and writes periodic snapshots.
- History. Positions that cannot be predicted from the last one are written to PostGIS in daily partitions.
- API ingest. The API subscribes to the live updates and holds the whole live set in memory.
- History reads. Tracks, replay frames and heat cells are read from PostgreSQL on demand.
- Delivery. Changes stream to each viewer through Cloudflare; static and history responses are cached at the edge.
The same flow, one stage at a time.
Every stage does one job and hands a smaller, cleaner stream to the next.
- 1
Sensors and sources
- 1090 MHz ADS-B and Mode S from SDR receivers
- 162 MHz AIS from maritime receivers
- CelesTrak orbital elements
- AISHub vessel exchange
- Airports, weather, aircraft database
- 2
Ingest
- readsb: Beast in, tracked aircraft out
- mlat-server-rs: clock sync and multilateration
- AIS NMEA over UDP
- 3
Data engine
- Aircraft, maritime and space workers
- One object model for all three
- Redis live state, PostGIS history
- Enrichment, routes, metrics
- 4
Bespoke API
- Every live object in memory
- WebSocket viewport deltas
- Level-of-detail tiers
- JSON, tracks, history, search
- 5
Clients
- Map, a modified tar1090
- Telemetry tables and graphs
- Replay and heat maps
- Installable phone app
Local SDR: from antenna to Beast.
Everything starts at a volunteer’s antenna. An aircraft’s transponder broadcasts ADS-B and Mode S on 1090 MHz; vessels broadcast AIS in the maritime band around 162 MHz. A software-defined radio digitises a slice of spectrum, and software does the rest.
The receive chain matters more than anything downstream, and it has the same shape on every band: antenna → filter → low-noise amplifier (LNA) → SDR → decoder software. The antenna decides what can be heard. The band-pass filter stops strong out-of-band signals from overloading the receiver. The LNA, placed ahead of cable loss, sets the noise floor. The SDR digitises the band, and software decodes it. Range comes from getting each link right, not from more power.
Demodulation and decoding. The decoder software (readsb for 1090 MHz) samples the radio at 2.4 Msps, finds Mode S preambles, checks the 24-bit CRC and attempts single-bit error correction. It decodes DF17/18 extended squitters, resolves compact position reporting (CPR) pairs into latitude and longitude, and keeps a per-aircraft state. Messages leave the receiver in Beast binary format, which carries the 12 MHz timestamp on every frame that multilateration needs.
Sharing. Most feeders run the SDR-Enthusiasts Ultrafeeder image. Two lines send data to the network:
ULTRAFEEDER_CONFIG= adsb,feed.mlat.uk,30004,beast_reduce_plus_out; # ADS-B (Beast) mlat,feed.mlat.uk,31090; # MLAT timestamps
AIS receivers (AIS-Catcher or Shipfeeder) send NMEA sentences to feed.mlat.uk over UDP. No account or key is needed; the contributor guide has the details.
At the receiver
- ADS-B / Mode S
- 1090 MHz, PPM, 1 Mbit/s frames of 56 or 112 bits
- AIS
- 161.975 and 162.025 MHz, GMSK at 9,600 baud, 25 kHz channels
- RF chain
- Antenna, filter, LNA, SDR, decoder software, on every band
- Sample rate
- 2.4 Msps for 1090 MHz
- Decoder
- readsb,
beast_reduce_plusoutput - Time base
- 12 MHz Beast timestamp on every frame
- Transport
- TCP (Beast, MLAT), UDP (AIS NMEA)
readsb: the front door.
Every feeder’s Beast stream lands on a central readsb, readsb, the decoder most of the community uses. It decodes, de-duplicates and tracks: if forty receivers hear the same aircraft, the network still holds one record, with the best position, a message count and the strongest signal.
readsb also carries the multilaterated positions. The MLAT server (next section) writes its results back to readsb as Basestation (SBS) lines, so a position worked out from timing differences appears in the same track as an ADS-B one, flagged as mlat.
From there, readsb pushes only aircraft that have new data to the data engine over a JSON port, and passes client and receiver state to the Telemetry page. The hot decode thread is the capacity limit on the aggregation side, so it is measured continuously and capacity is planned against it.
Aggregation, in practice
- Input
- Beast from every feeder, around 24,000 messages per second at 139 receivers
- Decoder
- readsb, running as the central aggregator
- MLAT results in
- SBS from mlat-server-rs
- Output
- A stream of changed aircraft, plus client and receiver state
mlat-server-rs: positions without a transmitter.
Many aircraft, especially older ones, send Mode S replies that carry no position. But if three or more receivers hear the same transmission, the differences in arrival time fix where it came from, because radio travels about 300 metres per microsecond. That is multilateration (MLAT).
The hard part is time. Receivers do not share a clock, so the server must learn each pair’s clock offset and drift. It does this from aircraft that do broadcast their position: the known geometry tells it when a signal should have arrived, and the difference is a measurement of the two clocks.
The original is wiedehopf’s fork of Oliver Jowett’s Python mlat-server. It is single-core bound, so mlat.uk runs mlat-server-rs, a multithreaded Rust rewrite that speaks the same receiver protocol and Basestation output, so no feeder had to change anything.
The pipeline
- Connection handling. Parallel I/O threads decode each receiver’s zlib-framed protocol.
- Clock synchronisation. Receiver pairs are sharded across clock workers, each maintaining an offset-and-drift model with outlier rejection.
- Matching. Aircraft are sharded by ICAO address; copies of one transmission from different receivers are grouped within a time window.
- Solving. A least-squares solve (MINPACK
lmdif, a Levenberg–Marquardt method) turns the arrival-time differences into a position and altitude. - Smoothing. An unscented Kalman filter tracks each aircraft over time and rejects bad fixes.
- Output. Filtered positions go to readsb as Basestation lines; per-receiver results and health state go back to feeders.
Measured, not claimed
- CPU, live
- 49% → 21% of a core for the same traffic, with 7% more receivers (2.2×)
- CPU, replay
- 14 minutes of production traffic: 148 s → 60 s (2.5×)
- Position maths
- 13.9 s → 2.6 s on the same input (5×)
- Agreement
- Median 12 m from the original; the original differs from itself by 5–6 m
- Determinism
- All 1,830 results reproduced exactly against a virtual-clock run of the Python pipeline
- Against ADS-B
- Median error about 105 m, equal to the original, and an upper bound because the truth carries network jitter
Most of the server’s CPU is network and protocol work, not the maths, which is why the whole-server saving is smaller than the saving on the position solve. Licensed AGPL-3.0-or-later.
Open source, as a gift to the community. mlat-server-rs is public so that anyone running a multilateration network can use it, learn from it or adapt it.
One picture from three kinds of data.
The data engine is what turns ADS-B tracks, AIS sentences and orbital elements into a single common operating picture. It is bespoke: three domains, one object model, one stream, one set of rules for what is important.
Aircraft
Reads the readsb JSON stream. Ingest is sharded by ICAO hex across worker threads, one per CPU less one. Records are enriched from the tar1090 aircraft database (registration, type, operator, military and interesting flags) when a feed lacks them.
Vessels
Decodes AIS NMEA from local receivers over UDP, optionally pulls AISHub, and gives local feeds priority over the exchange so a vessel is never double-reported. Static data (name, dimensions, destination) merges with position reports by MMSI.
Satellites
Loads CelesTrak orbital elements and the SATCAT catalogue, groups them, and propagates positions with SGP4. The same elements are published to browsers, so clients can compute positions locally, and any past position, with no history stored.
How the stream is kept small
- Trimmed copies. Each update is published with only the fields the map draws; the full record is stored only for the object a viewer has selected.
- Delta-only. Objects whose published fields have not changed are held back, with a periodic heartbeat so late joiners and expiry stay correct.
- Pub/sub plus snapshots. Updates fan out through Redis; workers also write periodic snapshots so a restarting service picks up where it left off.
- One live set. The API owns the in-memory picture and answers viewing queries itself.
Supporting workers
- Route cache. Resolves callsign to origin and destination with LRU, Redis and PostgreSQL layers, and drops routes that do not fit the aircraft’s position and heading.
- Airdata. Airports, METAR and TAF, SIGMETs, space weather and the aircraft database, refreshed on schedules.
- Feeders and metrics. Contributor stats, MLAT sync links and site-wide figures that drive the Telemetry page.
- Housekeeper. Daily partitions, retention and heat-map roll-ups.
History without the weight
Positions are written to PostgreSQL with PostGIS, partitioned by day so retention is a cheap drop. Only positions that cannot be predicted from the previously stored one are kept (turns, climbs, speed changes, plus at least one per minute), so straight flight costs almost nothing. Replay reads ten-minute chunks of this history; heat maps read hourly cells rolled up from it and merged into daily cells as they age. Satellites are never stored: their orbits are the history.
How three domains become one.
An aircraft, a ship and a satellite have nothing in common physically. The engine gives them one shape: an id, a position, a motion vector, a tier and a lifetime. After that, everything downstream treats them alike.
| Air | Maritime | Space | |
|---|---|---|---|
| Identity | ICAO 24-bit hex (~ prefix for non-ICAO) | MMSI | NORAD catalogue number |
| Source of truth | Volunteer ADS-B and MLAT, via readsb | Local AIS receivers, then AISHub | CelesTrak orbital elements |
| How a position arises | Received, or multilaterated from timing | Received AIS position reports | Computed with SGP4 from elements, server and browser |
| Update model | Push, roughly every second per aircraft | Push, every few seconds to minutes per vessel class | Recomputed continuously, no feed to wait for |
| Tier 0 (always shown) | Military, emergency squawks 7500, 7600, 7700 | Military, active distress beacons | Military and space stations |
| History | 48 hours by default, thinned by predictability | 72 hours by default, thinned | None stored: orbits are the history |
| Enrichment | Registration, type, operator, route | Name, dimensions, destination, flag state | Owner, launch, orbit class, element age |
Normalise
Each worker maps its source into the shared object shape and a single time base, in the same units: feet, knots, degrees, Unix seconds.
Rank
Every object is given a tier, so the API can decide what matters at each zoom without knowing what kind of thing it is.
Stream as one
One WebSocket carries all three layers. The browser uses the same selection, labels, tracks and search for a ship as for an aircraft.
A bespoke, streaming API.
The API is written for exactly one job: keep a map current, for many viewers at once, without wasting a byte. It holds every live object in memory and does its own spatial filtering, because a general-purpose database cannot answer “what is in this box, at this zoom, that I have not already been told” as cheaply.
Only what you are looking at. A viewer tells the server which part of the world it is viewing and which layers it wants. The server sends what is in view, then only what changes, so a phone receives a few kilobytes a second rather than the whole picture again.
Level of detail. Every object is ranked, from the things that always matter (military, emergencies, stations) down to everything else. Zoom decides how deep into that ranking a viewer sees: a world view shows what matters, and zooming in adds the rest, without objects flickering in and out as they change.
One service for everything else. The same API serves the Telemetry tables, recorded tracks, replay history, search and the reference datasets, compressed and cached at the Cloudflare edge wherever it can be, and the live streams are limited to the mlat.uk sites.
API at a glance
- Live
- One stream per viewer, answered from memory
- Detail
- Ranked by importance, revealed by zoom
- Identity
- One namespace per layer: aircraft, vessels, satellites
- History
- Recent aircraft and vessel history; satellites computed
- Replay
- Stored positions replayed in short chunks; heat maps from rolled-up cells
- Edge
- Cloudflare in front
A map that grew out of tar1090.
The map began as tar1090, wiedehopf’s fast, WebGL-accelerated aircraft map, and it still carries tar1090’s object model, layers and keyboard shortcuts. mlat.uk’s changes are published under the same GPL.
The change that makes it multi-domain is at the seam: the WebSocket client feeds objects into the same code path a readsb poll would, so vessels and satellites reuse the aircraft machinery for selection, labels, tracks and search. On top sit satellites propagated in the browser, a first-person Horizon and Sky view, replay and heat maps, and an installable full-screen app.
Static at the edge, live from the stream.
This website and the map are static assets served by Cloudflare Workers, so the pages load from the edge nearest you. Only the live data touches the origin: one WebSocket per viewer, answered from memory.
Because the heavy work (decoding, multilateration, fusion, filtering) is done once on the server, a phone receives a few kilobytes a second for the part of the world it is looking at.
One engineer. The whole stack.
There is no company, team or organisation behind mlat.uk. Every layer on this page was designed, written, tested and is run by one person, alongside the volunteers who feed it. The same person who stands at the antenna also wrote the server that locates aircraft and the code that draws them on your phone.
Radio
Antenna, filtering and low-noise amplification for 1090 MHz and AIS, tuned until reliable range went from around 50 to over 200 nautical miles. The community receiver network grew from that single station.
Multilateration
A multithreaded Rust rewrite of the MLAT server, validated against the original on recorded production traffic, then cut over live with no change for any feeder.
Data engine
The workers that turn ADS-B, AIS and orbital elements into one object model, with enrichment, route checks, tiering and delta-only publishing.
API
A streaming API built around a single question: what does this viewer need to see right now, and what have they not already been told?
Interface
The map, the Horizon and Sky views, replay and heat maps, the installable app, and this website, all on Cloudflare’s edge.
Operations
The container stack, capacity planning, load and replay benchmarks, monitoring and the graphs on the Telemetry page, so there is always a measured answer to “will it scale?”
That is also why it is built the way it is: each stage has one clear job, measured against the one before it, and nothing is added that cannot be explained on a single page. If you run a receiver, you are part of it.
Built on other people’s work.
mlat.uk exists because of open-source software and open data. Thank you to everyone below.
| Source | Used for | Licence / terms |
|---|---|---|
| tar1090 | The inspiration and base of the map: object model, layers and interface. Modified for mlat.uk, with the changes published under the same licence. | GPL-2.0+ |
| readsb | Mode S and ADS-B decoding on receivers and the central aggregator. Derived from dump1090 and the original readsb. | GPL-2.0+ |
| mlat-server | The reference multilateration server and receiver protocol (wiedehopf’s fork of Oliver Jowett’s), which mlat-server-rs reimplements. | AGPL-3.0 |
| mlat-server-rs | The Rust multilateration server running on mlat.uk. | AGPL-3.0+ |
| SDR-Enthusiasts Ultrafeeder | The feeder container most contributors run, and the community that supports it. | Open source, see repository |
| tar1090-db | Aircraft registration, type, operator and military flags. | Open source, see repository |
| satellite.js | SGP4/SDP4 satellite propagation in the browser. | MIT |
| d3-celestial | Star chart data (Hipparcos, ESA) for the Horizon and Sky views. | BSD-3-Clause |
| AISHub | Vessel positions and static data exchanged between contributors, merged with local AIS receivers. | Data sharing agreement; credited as Data: AISHub contributors |
| CelesTrak | Orbital elements (OMM/TLE) by group, and the SATCAT catalogue behind every satellite’s details. | Public data, credited |
| NOAA Aviation Weather Center | METAR, TAF and SIGMET hazard areas. | US Government public data |
| NOAA Space Weather Prediction Center | Planetary K-index and the OVATION aurora forecast. | US Government public data |
| OurAirports | Airport catalogue with runways and frequencies. | Public domain |
| adsb.im | Callsign-to-route lookups, cached and sanity-checked against the aircraft’s position and heading. | Service terms apply |
| OpenStreetMap and OpenFreeMap | Base maps and the building footprints in the 3D Horizon view. | ODbL © OpenStreetMap contributors |
| Open-Meteo | Tide, sea state and weather on port cards. | CC BY 4.0 |
| Wikimedia Commons | Vessel and satellite images, and community photo sites for aircraft, each credited to its photographer. | Per-image licence |
mlat.uk is privately owned and independently operated. The data on the map comes from a network of volunteer contributors, augmented with the public sources above. It is for personal, non-commercial situational awareness only. See the privacy policy.
Add a receiver to the network.
Every antenna extends the picture, and three more receivers in an area is what turns on multilateration there. It takes a few minutes with Docker.