Week 19 · Mission GIS Engineer

Real-time GIS: WebSockets and streaming TLEs

Real-time isn't optional in serious space GIS. This week you build the WebSocket pipeline that streams live satellite positions to a browser, throttling and reconnecting.

Learning objectives

Primer

Real-time is not optional in serious space GIS. A launch detection that takes 5 minutes to appear in a user's browser is much less valuable than one that appears in 5 seconds. This week you build the WebSocket pipeline that makes this possible.

WebSocket vs polling vs SSE

Three options for getting server data to a browser in near-real-time:

FastAPI as a WebSocket server

FastAPI (fastapi.tiangolo.com) has first-class WebSocket support — declarative, async, type-checked. The skeleton:

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio, json
from skyfield.api import EarthSatellite, load, wgs84

app = FastAPI()
ts = load.timescale()
sats = [EarthSatellite(l1, l2, name, ts) for name, l1, l2 in tle_catalog]

@app.websocket("/ws/positions")
async def positions(ws: WebSocket):
    await ws.accept()
    try:
        while True:
            t = ts.now()
            payload = []
            for sat in sats[:100]:
                sp = wgs84.subpoint(sat.at(t))
                payload.append({
                    "name": sat.name,
                    "lat": sp.latitude.degrees,
                    "lon": sp.longitude.degrees,
                    "alt_km": sp.elevation.km
                })
            await ws.send_text(json.dumps(payload))
            await asyncio.sleep(1.0)  # 1 Hz throttle
    except WebSocketDisconnect:
        pass  # clean disconnect — nothing to do

The browser side

The browser's WebSocket constructor opens the connection. On each message, update marker positions. Throttle DOM updates if needed (1 Hz to the server is fine; rendering at 60 fps in the browser is overkill — animate-tween between samples for smoothness):

const ws = new WebSocket('wss://launchdetect.com/ws/positions');
ws.onmessage = (ev) => {
  const positions = JSON.parse(ev.data);
  for (const p of positions) {
    const marker = markers.get(p.name);
    marker?.setLngLat([p.lon, p.lat]);
  }
};
ws.onclose = () => setTimeout(reconnect, 1000 * Math.min(retryCount++, 30));

Throttling and back-pressure

10,000 satellites × 60 Hz = 600,000 updates/second. No browser will keep up; no server should send that. Realistic limits:

Reconnection

WebSockets disconnect for many reasons: WiFi flap, server restart, mobile network switch, load balancer hiccup. A production client must reconnect with exponential backoff (1 s, 2 s, 4 s, 8 s, ..., capped at 30 s) and detect close vs error events. Socket.IO (socket.io) bundles reconnection, namespaces, rooms, and HTTP-polling fallback — at the cost of a heavier wire protocol. For pure server-to-client streaming, raw WebSockets are usually enough.

The lab

You'll build a FastAPI WebSocket endpoint that streams positions of 100 satellites at 1 Hz, and a MapLibre client that receives the stream and updates marker positions in real time. Handle disconnects gracefully with exponential backoff. This is the architecture LaunchDetect uses to stream live detection events to the browser — minus authentication and selective subscription, which are Track 5 topics.

Hands-on lab: Live 100-satellite stream

Build a FastAPI WebSocket endpoint that streams positions of 100 satellites at 1 Hz. Build a MapLibre client that receives the stream and updates marker positions. Handle disconnects.

Quiz

Test yourself. Answer key on the certificate-track page (Gold-tier feature: progress tracking and auto-grading).

Q1. WebSocket is:
  1. Bidirectional persistent connection over a single TCP connection
  2. Just HTTP polling
  3. UDP only
  4. Server-side only
Q2. Why throttle update rate to the browser?
  1. High update rates degrade browser rendering and battery
  2. WebSockets are slow
  3. Required by spec
  4. Servers can't handle it
Q3. Socket.IO adds what over raw WebSockets?
  1. Reconnection, namespaces, rooms, fallbacks
  2. Just JSON encoding
  3. Encryption only
  4. Compression only
Q4. For 10,000 satellites at 1 Hz, what's the bottleneck likely?
  1. Client rendering (DOM/WebGL), not WebSocket throughput
  2. WebSocket protocol
  3. Server CPU only
  4. DNS
Q5. Handling disconnects requires:
  1. Detecting close events and re-establishing with backoff
  2. Trying again every 10 ms
  3. Ignoring the issue
  4. Reloading the page only