The 100 problems, solved / Maps and location / 041 · Visualize 100 million points on a map

Visualize 100 million points on a map

Cluster, tile and stream points so the browser never holds more than it can draw.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Show 100 million points (say, every delivery address or every sensor reading) on an interactive map.
  • Zoomed out, show clusters with counts; zoomed in, show individual points with details on click.
  • Filter by attribute (category, date range) and see the map update.
  • Points are updated in bulk a few times a day.
  • Out of scope: editing points in the browser, heat maps as a separate mode, 3D.

Non-functional

  • A pan or zoom shows new content within 200 ms on a laptop and a mid-range phone.
  • The browser never holds more than about 50,000 features at once; memory under 300 MB.
  • 10,000 concurrent users; the backend serves them from caches, not from a database per pan.
  • A filter change must not require re-downloading the whole dataset.
  • Bulk updates become visible within an hour.
Back of the envelope

100 M points × 50 B (coordinates plus a few attributes) = 5 GB raw. A browser can draw perhaps 100,000 markers before it stutters, so at most 0.1 percent of the data may ever be on screen at once.

Tiles: at zoom 14, a city is about 100 tiles; a 20 KB vector tile per view tile is 2 MB per screen, which is why tiles must be pre-aggregated and small.

Pre-aggregation: clusters for zooms 0 to 12 over 100 M points produce a few million cluster records in total; a batch job computes them in minutes.

10,000 users panning every 2 s is 5,000 tile requests a second, trivially served from a CDN at a high hit rate since the tiles change a few times a day.

2. Core entities

  • Point: point_id, lat, lon, category, timestamp, small attribute set, detail reference.
  • Cluster: zoom, cell, count, centroid, per-category counts.
  • Tile: zoom, x, y, dataset version, filter dimension (optional), vector payload.
  • Dataset version: version id, build time, point count.

3. API

  • GET /tiles/{dataset}/{v}/{z}/{x}/{y}.pbf?category=…: vector tile with clusters or points. Served by the CDN.
  • GET /points/{id}: full detail for a clicked point.
  • GET /datasets/{dataset}: current version and the available filter dimensions.
  • GET /search?bbox=…&from=…&to=…&limit=…: for filters that are not pre-tiled, a bounded query against the index.

4. High-level design

BrowserEdgeServicesDatatilesmisspublishloaddetail, searchMap UI(WebGL layer)CDNTile + detail APITile builder(batch, per version)Ad-hoc query serviceTiles by versionPoints store(geospatial index)Bulk source files

A user zooms into a city:

  1. The UI asks the dataset endpoint once for the current version and keeps it for the session so every tile request is cacheable and consistent.
  2. For the viewport it computes the tile coordinates at the current zoom and fetches each tile from the CDN; at zoom 8 the tiles contain clusters with counts, drawn as circles sized by count.
  3. Zooming to 15 fetches point tiles, each holding at most a few thousand points with minimal attributes; the UI draws them on a WebGL layer rather than as DOM markers.
  4. Clicking a point calls the detail endpoint, the only per-point request the system makes.
  5. Selecting a category filter switches the tile URL to the pre-built category variant; a date-range filter that has no pre-built variant falls back to the query service, bounded by the viewport and a result cap.

5. Deep dives

Why precompute clusters instead of clustering in the browser or on request?

OptionBehaviourTrade-off
Download all points, cluster in the browser5 GB download; memory and CPU explodeOnly works below about 100,000 points
Cluster on the server per requestA database aggregation over the viewport per pan200 ms is impossible at zoom-out over 100 M points, and 5,000 requests a second hit the database
Precompute cluster tiles per zoom in batchA few million records built in minutes; served as static tilesClusters are a few hours stale; filters need their own tile sets or a fallback

Precompute; the data changes a few times a day and the read volume is high, which is exactly when precomputation wins. Clusters are built hierarchically: aggregate points into zoom-16 cells, then merge cells four at a time up to zoom 0, so each level is a pass over the level below.

How does the browser stay under 50,000 features?

At every zoom, each tile is capped: cluster tiles hold at most a few hundred clusters, point tiles at most a few thousand points, and a viewport is at most about 20 tiles. If a point tile would exceed its cap (a dense city block at zoom 15), the builder replaces part of it with clusters even at that zoom, so the cap holds everywhere. Tiles outside the viewport plus a one-tile margin are dropped from memory as the user pans, and the WebGL layer draws points as instanced sprites, which is how 50,000 points render in a few milliseconds.

How do filters work without exploding the number of tile sets?

A filter dimension with a small number of values (10 categories) gets a tile set per value, 10 times the build cost and storage but still small. A continuous dimension like date cannot be pre-tiled for every range; the builder instead pre-tiles a handful of common ranges (today, this week, this month) and everything else goes to the query service, which runs a bounded geospatial query (viewport, range, limit 5,000) against the points store and returns clusters computed on the fly for that viewport only. The UI shows a note that the view is sampled when the cap is hit.

How are updates published without a flicker or a mixed map?

Each build produces a complete tile set under a new version; the dataset endpoint flips the version only when every tile is written. Sessions keep using their version until they reload, so a user never sees tiles from two builds. Old versions are deleted after a day. This is the same versioning trick map tiles use and it also makes the CDN cache trivially correct: versioned URLs never change meaning.

What does the detail path need?

Details are the one place the system reads a single record. The points store is keyed by point_id and holds the full attribute set; it is sharded by id and cached in front, since clicks concentrate on the same popular points. Nothing on the tile path ever touches it.

6. What a strong answer sounds like

  • Open with the constraint that drives everything: the browser can show 0.1 percent of the data, so the server must decide what to send, by zoom.
  • Quote 5 GB raw, 20 KB per tile, 20 tiles per view, 5,000 tile requests a second from a CDN.
  • Explain hierarchical cluster building and per-tile caps.
  • Expect follow-ups on filters, on real-time updates (switch the hot subset to a live layer), and on rendering performance (WebGL, not DOM markers).
  • A common mistake is to begin with the database query for the viewport; the interviewer wants to hear tiles and precomputation first.

02Where this question was reported