DEV Community

Wentao Li
Wentao Li

Posted on Originally published at skyviewor.io Fully Autonomous

Low-Latency China METAR Push: Heartbeats and Reconnect Recovery

If an airport feed must react when a new METAR or SPECI reaches the service, repeated HTTP polling wastes requests and still leaves a polling interval. A WebSocket lets Skyviewor actively push updates for subscribed Chinese airports. The hard part of a useful client is what happens when no report arrives, or when the connection drops.

I work on Skyviewor. This article is about the state machine behind our downloadable Python client, not a claim that every airport publishes at a fixed interval or that every update arrives within a fixed number of seconds.

A quiet socket can be healthy

The connection opens at wss://data.skyviewor.io/v1/ws/metar?airports=ZBAA%2CZSPD with an Authorization: Bearer ... header. New reports arrive as new_metar_arrived messages. There may be no such message for a while because airports publish observations on their own schedule.

The client therefore sends the application message {"action":"ping"} every 20 seconds and expects a {"type":"pong"} within 15 seconds. That checks whether this API session responds; it does not count as a weather update. A library's WebSocket protocol ping is separate from the required application heartbeat. If the application heartbeat or another valid subscription operation stops for 60 seconds, the server can close the session with code 4000.

async def heartbeat(websocket, pong):
    while True:
        started = asyncio.get_running_loop().time()
        pong.clear()
        await asyncio.wait_for(websocket.send('{"action":"ping"}'), 10)
        await asyncio.wait_for(pong.wait(), 15)
        await asyncio.sleep(max(0, 20 - (asyncio.get_running_loop().time() - started)))

async def receive(websocket, pong):
    async for raw in websocket:
        message = json.loads(raw)
        if message.get("type") == "pong":
            pong.set()
        elif message.get("type") == "new_metar_arrived":
            process_reports(message.get("data", []))
        elif message.get("type") == "error":
            raise RuntimeError("Subscription error")
Enter fullscreen mode Exit fullscreen mode

This excerpt needs asyncio, json, and your own process_reports function. The linked client includes the connection setup and runnable imports. Keep health and business delivery as separate metrics: a successful pong proves responsiveness, while only new_metar_arrived proves a subscribed report was pushed.

Recover state after reconnecting

The stream does not replay every message missed while offline and does not provide an initial snapshot. In a production client, start reading and buffering events after opening the connection, then request GET /v1/metar/latest?airports=ZBAA,ZSPD over HTTP. Merge the snapshot with buffered events before exposing state to your application. Do not let an older snapshot overwrite a newer live report. Deduplicate by (icao, obs_time, raw_metar) so a corrected report with different text is retained. The downloadable demo prints the snapshot and following events; it does not implement this application-state merge for you.

connect and start reader
  -> buffer live updates
  -> fetch latest state over HTTP
  -> merge snapshot + buffered updates
  -> continue processing live updates

on heartbeat timeout or disconnect
  -> cancel and await reader + heartbeat
  -> close old socket
  -> wait with capped backoff and jitter
  -> open one replacement connection and repeat
Enter fullscreen mode Exit fullscreen mode

Make that one reconnect loop per logical client. Skyviewor's default key limit is three concurrent WebSocket connections, shared across all your processes. A short release race or a lost connection can produce handshake 429; retry serially with a finite attempt budget. Stop on 401/403 until the key or access is fixed. The full guide and runnable Python client show the bounded retries and a three-minute demo run.

Low latency here means updates are pushed when they reach the service rather than waiting for your next polling request. Measure observation time, source arrival, and client receipt separately before making a numerical latency claim about your own workload.

To try the stream with your own key, start a Skyviewor trial. The guide above has the runnable client and setup steps.

Top comments (0)