Week 19 · Mission GIS Engineer~9 min · 970 words

Real-time GIS: PostGIS LISTEN/NOTIFY and WebSocket fan-out

Real-time isn't optional in serious space GIS. This week you build the full production pipeline: a detection writes to PostGIS, the database broadcasts via LISTEN/NOTIFY, a WebSocket server fans the event out to every connected browser in under 100 ms. Same pattern used by every real-time geospatial dashboard in production.

When a tsunami warning gets issued in the Pacific, how does the alert reach your phone in seconds, not minutes?

Real-time geospatial systems push events from where they're detected (a buoy, a seismometer) to where they're needed (your phone, the school siren). WebSockets are how. This week you'll build the same pipeline.

Learning objectives

PTWC on Ford Island (Pearl Harbor) — the heart of Pacific tsunami early warning since 2014, when the center moved from its original ʻEwa Beach campus. Your code this week uses the same real-time architecture.

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:

  • HTTP polling — client fetches once per N seconds. Simple, works through any proxy, but inefficient for high-frequency updates and high latency (you're at most 1 polling interval behind reality).
  • Server-Sent Events (SSE) — server pushes events over a long-lived HTTP connection. Works through proxies and CDNs (it's just HTTP), simpler than WebSockets, but one-way (server → client only).
  • WebSocket — bidirectional persistent TCP connection. The right choice when you need server → client at high frequency AND occasional client → server messages. Most space-domain live trackers use WebSockets.

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:

  • Server: 1–4 Hz aggregate update cadence is typical. SGP4 is fast (microseconds per satellite), so the server can compute. The bottleneck is network bandwidth and client capacity.
  • Client: roughly 1,000 marker updates per frame is the ceiling on modern hardware. For 10,000 satellites at 1 Hz, the client needs to spread updates across multiple frames or batch them via WebGL.

Where the events come from: PostgreSQL LISTEN / NOTIFY

The example above generates positions in-process — a useful demo, but not how a production system works. In real life, a separate detection pipeline (the one you'll build in Week 27) writes new records to PostGIS, and your WebSocket server has to learn about those writes without polling. PostgreSQL ships with a built-in pub/sub mechanism — LISTEN / NOTIFY — that solves this elegantly:

-- in PostgreSQL: a trigger that publishes new rows
CREATE OR REPLACE FUNCTION notify_detection() RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify('detections',
    json_build_object('id', NEW.id, 'lon', ST_X(NEW.geom), 'lat', ST_Y(NEW.geom),
                      'detected_at', NEW.detected_at)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER detections_notify AFTER INSERT ON detections
FOR EACH ROW EXECUTE FUNCTION notify_detection();

Now any INSERT on the detections table broadcasts a JSON payload on the detections channel. The WebSocket server subscribes:

import asyncpg, json
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket

subscribers: set[WebSocket] = set()

async def on_notify(conn, pid, channel, payload):
    msg = payload  # already a JSON string from pg_notify
    dead = set()
    for ws in subscribers:
        try: await ws.send_text(msg)
        except Exception: dead.add(ws)
    subscribers.difference_update(dead)

@asynccontextmanager
async def lifespan(app: FastAPI):
    conn = await asyncpg.connect(DSN)
    await conn.add_listener('detections', on_notify)
    yield                       # app runs
    await conn.close()          # graceful shutdown

app = FastAPI(lifespan=lifespan)

@app.websocket("/ws/detections")
async def detections(ws: WebSocket):
    await ws.accept()
    subscribers.add(ws)
    try:
        await ws.receive_text()  # block until client disconnects
    finally:
        subscribers.discard(ws)

Three reasons this pattern is good: (1) zero polling — events propagate at the latency of a Postgres transaction commit, (2) the WebSocket layer doesn't need to know anything about the detection pipeline, it just relays, (3) it scales horizontally: every WebSocket server instance listens to the same channel, and Postgres handles the fan-out. For cross-instance fan-out at higher scale (thousands of concurrent connections), pair PostgreSQL with a Redis pub/sub layer or move to a managed event bus — Week 27's EventBridge pattern is the AWS-native version of the same idea.

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.

On the server side, also think about resume semantics after reconnect. If a client was offline for 30 seconds, do they want the last 30 seconds of events replayed, or just the live feed from now on? Encode the answer in the URL: /ws/detections?since=2026-05-12T18:00:00Z lets the server query PostGIS for events since that timestamp before flipping the socket to live mode. That replay-then-live pattern is the standard real-time GIS contract.

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. Bonus track: wire PostgreSQL LISTEN/NOTIFY so writes to a detections table fan out to all connected browsers in under 100 ms. This combined pattern (PostGIS as the source of truth, WebSocket as the live transport) is the foundation of every real-time geospatial dashboard in production — from tsunami early warning to fleet tracking to launch detection.

Connecting to Hawaiʻi: PTWC and Pacific real-time alerts

The Pacific Tsunami Warning Center, headquartered since 2014 at the NOAA Inouye Regional Center on Ford Island, Pearl Harbor (and at its legacy ʻEwa Beach campus before that), issues warnings for the entire Pacific basin. When their seismologists detect a M7+ earthquake, alerts have to reach ~50 Pacific Island nations within minutes. The technical stack uses real-time message-passing — the same WebSocket pattern you'll learn this week. WebSocket is also how LaunchDetect streams launch detections, how Pacific Disaster Center streams damage assessments, how RealEarth streams GOES imagery. Real-time GIS is critical for everyone living around the Pacific Ring of Fire.

tsunami.gov shows PTWC's live alerts. Their system architecture is publicly documented — and it's structurally identical to what you're learning.

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 — click an answer to check it

No grade, no shame. Tap any option; you'll see if it's right plus the answer if not. The point is to notice what you already know and what's still settling.

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

Reflection

Take five minutes with this. Write your answer somewhere. Carry it into next week.

Real-time data saves lives — and exposes them. Pacific peoples have always known the ocean is in motion; now we have a way to track every motion. What changes in our relationship to ocean when we can see all of it at once?
Mark this week complete Visiting alone doesn't count it as 'done'. Click when you've actually worked through the primer + lab + quiz.
Share + discuss on Twitter/X Discuss on GitHub