Serving 3.4 billion GPS points sub-second, in a browser tab

Serving 3.4 billion GPS points sub-second, in a browser tab

How Earth Traffic replays five years of a country's roads — and the cloud-native architecture that makes it queryable from a single map console

ELEMNT Engineering · ~9 min read


Figure 1 — Not a mock-up. A live snapshot pulled straight from Earth Traffic's public API (/api/v1/metrics/h3/heatmap): 5,000 GPS-probe cells for Bangkok at Wednesday 18:00, aggregated to H3 res-6 and coloured by travel-time index — a deep-red gridlocked core fading to green at the edges. Mean TTI 3.6, mean speed 27 km/h, 38% of cells jammed (LOS F). Data: iTIC Foundation (CC-BY-4.0).

Earth Traffic is a free, open archive of how the roads move. It takes 3.4 billion GPS probe points — five years of floating-car data covering all of Thailand — and lets you scrub through a typical week, or any specific hour across 2017–2022, on a calm, map-first console. No login, no paywall, nothing to install.

The interesting part isn't the map. It's that the whole thing answers queries over billions of rows in sub-second time, live, in the browser. This post is about how that's put together, and why a traditional geospatial stack can't do it.

Why this is hard

The naive version of this product is a tile server sitting in front of a spatial database. A request comes in for a map tile, the server runs a spatial query, rasterizes or vectorizes the result, and ships it back. That design is fine for thousands or even millions of features. It falls over well before billions.

Three things break at once. The per-request query cost is linear in the data you touch, so every pan and zoom re-pays it. The geometry is heavy — storing and intersecting raw polygons for a nationwide road network is expensive to read and slower to compute. And time makes it worse: five years of hourly data isn't one dataset, it's tens of thousands of them, and a per-request stack has to filter the haystack on every interaction.

You can't buy your way out of this with a bigger box. The fix is architectural: do the heavy work once, store it in a shape that's cheap to read, and serve precomputed results. That's the whole game.

Figure 2 — The shape of the workload. 3.4 billion probe points, five years of history, H3 res 5→11, all queryable sub-second in the browser. Real data, not a mockup.

The data

The raw input is GPS probe data — floating-car data from anonymous vehicles. Each point carries a position, a timestamp, and a speed. From those, the pipeline derives the metrics traffic engineers actually use: TTI (Travel Time Index), PTI (Planning Time Index), speed (km/h), and LOS (Level of Service, the A–F congestion grade). Those are the values you see painted on the map and read out in the console.

3.4 billion points is enough that the storage and query strategy matters more than any single clever query. So the design starts there.

Architecture in one picture

Figure 3 — The data path. Raw probe data → medallion ETL (bronze → silver → gold) → cloud-native columnar engine → live MVT → MapLibre in the browser. The heavy compute runs once, at build time — not per query.

The flow is left to right, and the single most important property is written across the bottom of the diagram: heavy compute runs once at build time, not per query. Everything else follows from that.

1. Pre-aggregate at build time (the medallion pipeline)

Ingestion follows a medallion pattern. Bronze is the raw probe points, landed as-is. Silver is cleaned and conformed — bad fixes dropped, timestamps normalized to UTC, speeds sanity-checked. Gold is the payload the app actually serves: metrics pre-aggregated per H3 cell, per hour. The expensive spatial-temporal aggregation happens here, in batch, exactly once. By the time a user opens the map, the answer to "what was congestion in this hexagon at 14:00 on a Wednesday" has already been computed and stored.

This is the inversion that makes sub-second possible. A per-request stack computes the answer when you ask. Earth Traffic computes it when it builds, and at query time it's doing a lookup-and-aggregate over precomputed rows, not a raw spatial join.

2. H3 as the spatial index

The spatial index is H3, Uber's hierarchical hexagonal grid. Instead of storing road geometry as polygons, every probe point is quantized into the hexagon that contains it — at several resolutions at once, from res 5 (coarse, national) down to res 11 (fine, street-level).

Figure 4 — H3 multi-resolution indexing. The same area, re-quantized from res 5 (national) to res 11 (street). Zoom decides which resolution is served — one coordinate system, no raw polygon geometry to intersect.

This buys two things. First, the index is just an integer per cell, so there's no polygon geometry to store or intersect — joins become hash lookups. Second, resolution maps cleanly onto zoom: at the national view you serve coarse cells, and as the user zooms in you serve finer ones, all in the same coordinate system. The grid "re-quantizes" itself across zoom levels with no seams and no special-casing.

3. Columnar OLAP, sub-second

At query time, a cloud-native columnar geospatial engine does the aggregation. Columnar storage is the right tool here because the queries are analytical: aggregate one or two metrics across a slice of cells and hours. A column store reads only the columns a query touches and skips the rest, so a query over a billion-row table scans a few narrow columns rather than the whole width. Combined with the gold layer's pre-aggregation, that's what lands the response in the sub-second range.

4. Serve as live MVT

The result of each query is wrapped as a Mapbox Vector Tile (MVT), generated on the fly per z/x/y. The browser receives the hexagon geometry plus its properties — tti, los, speed, probes — and renders it client-side with MapLibre GL. No image tiles, no server-side rendering: the client gets data and decides how to draw it.

The trick worth stealing: one tile source, every metric

Here's the design decision that makes the UI feel instant. The tiles don't encode a rendered metric — they carry every metric in their properties. The map has a single tile source, and switching what you're looking at is purely a client-side restyle.

Figure 5 — One tile source carries every metric in its properties. Switching TTI ↔ speed ↔ LOS is a client-side style-JSON paint swap — zero tile re-fetch, one ~16 ms frame.

In MapLibre, the colour of a layer is a data-driven expression over a feature property. Painting by TTI looks like this:

{
  "id": "congestion",
  "type": "fill",
  "source": "earth-traffic",
  "source-layer": "hex",
  "paint": {
    "fill-color": [
      "interpolate", ["linear"], ["get", "tti"],
      1.0, "#34d383",
      1.5, "#f5c542",
      2.5, "#ff564e"
    ]
  }
}

To switch the entire map from TTI to speed, you change ["get", "tti"] to ["get", "speed"] (and its stops) and call setPaintProperty. That's it. The tiles already on the GPU don't move; no new network request is made. The layer re-colours in a single animation frame — about 16 ms — because you changed the paint, not the data. The same property bag also powers the hover tooltip, which is why pointing at any hexagon resolves its numbers in one frame too.

Time travel

The time dimension is a first-class control, not a filter buried in a menu. The scrubber steps any hour across the full five years; a density sparkline above it shows which hours actually have probe coverage, so you're never fooled by a quiet hexagon that's really just a gap in the data. The whole thing is keyboard-driven — space to play, ← / → to step one hour — because anyone who watches traffic patterns for a living wants to scrub frame by frame, not hunt for a slider with a mouse.

Open by default

Earth Traffic is built standards-first, so the data doesn't dead-end at the screen. It speaks OGC API – Features, and you can stream the same queries out as GeoJSON, CSV, or NDJSON — no auth required. Timestamps are UTC ISO-8601, coordinates are EPSG:4326, speeds are km/h. The request pattern is the ordinary OGC one — a bounding box and a datetime against a collection:

# representative — a bbox + hour, streamed as newline-delimited JSON
curl "https://traffic.elemnt.earth/collections/hex/items\
?bbox=100.3,13.5,100.9,14.0\
&datetime=2019-11-04T08:00:00Z\
&f=ndjson"

Most map platforms make you screen-scrape or pay per request to get data back out. This is the opposite stance: open formats, an open API, build on top immediately.

The console as an instrument

All of this is wrapped in a deliberately calm, dense UI — an operations console for city infrastructure rather than a dashboard. Around thirty live values update at once, monospaced and exact: zoom, active H3 resolution, the hour you're parked on, the query latency, the tile source. It's designed to be read the way an engineer reads an instrument, not skimmed the way you skim a brochure. That's the honest summary of the whole project: at this scale, the architecture is the product.

Try it

traffic.elemnt.earth is live and free. Open it, grab the scrubber, and watch a country's roads move hour by hour — then switch metrics and notice that the map never re-fetches.


What is ELEMNT?

ELEMNT is the cloud-native geospatial data platform — open formats (PMTiles, COG, Zarr, GeoParquet) served straight from object storage, OGC API-compliant, with transparent pay-per-use pricing and no vendor lock-in. From a single project to a billion tiles. Earth Traffic is one demo of what it can do.