AIS: the one streaming source
aisstream.io over WebSocket — the streaming exception, and why it stays an exception.

Every source in World Monitor is a poller on a schedule — except one. AIS (ship positions) arrives over a WebSocket stream from aisstream.io, and it is the one streaming source. This post is about why it is the exception, and why it stays the exception.
Every source in World Monitor is a poller on a schedule — except one. AIS (ship positions) arrives over a WebSocket stream from aisstream.io, and it is the one streaming source. This post is about why it is the exception, and why it stays the exception.
Why AIS is different
AIS is a high-frequency positional stream: thousands of vessels, each emitting a position every few seconds. Polling it on a schedule would mean either a very short schedule (which is a polling loop pretending to be a stream) or missing positions between polls (which defeats the point). The honest design is to hold the WebSocket open and ingest positions as they arrive. That is what streaming.rs does — a persistent connection to aisstream.io, positions parsed into GeoSignal values as they land, the cache upserted on every message.

Why it stays the exception
The temptation, once you have one streaming source, is to stream everything. Resist it. A streaming source is a persistent connection — a resource you hold open for the lifetime of the process, with backpressure, reconnect, and partial-failure concerns that a poller does not have. Most feeds do not need it. Flights, quakes, weather, fire, conflict, satellites — these update on a schedule of minutes to hours, and a poller is the simpler, more robust choice. The line is: poll by default, stream only when the cadence demands it. AIS demands it; nothing else currently does.
Poll by default; stream only when the cadence demands it. A persistent connection is a cost you pay for every second you hold it — pay it only when the data rate earns it.
How the stream is bounded
Even the streaming source is bounded. The token-bucket budget that gates the polling sources does not apply directly — the stream is not a request — but the upsert into the Postgres cache is batched, and the GeoPublisher hands deltas on the same per-tile channel structure as the polling sources. A client on the realtime WS does not receive a firehose of AIS positions; it receives the deltas for its viewport tiles, same as every other source. The stream is bounded at the publish layer even when it is not bounded at the ingest layer.
- One streaming source, one persistent connection — AIS over WebSocket from aisstream.io.
- Polling is the default; streaming is the exception, earned by cadence, not by preference.
- The stream is bounded at publish: per-tile channels mean a client gets its viewport, not the firehose.
The operational cost
A streaming source is an operational commitment. The connection drops; you reconnect. The upstream throttles; you back off. The stream stalls; you detect it. Each of these is code in streaming.rs that a poller does not need. That cost is worth paying for AIS, where the cadence makes polling a lie. It is not worth paying for a feed that updates every fifteen minutes, where a poller is simpler, cheaper, and easier to reason about. The discipline is to add streaming sources reluctantly, and only when the data rate leaves no other choice.
A persistent connection is a cost you pay for every second you hold it. The polling sources pay nothing when idle. Know which one you are choosing, and why.
— streaming source design note
What it is not
This is not a claim that AIS data is accurate or complete. It is a feed of positions as reported by vessels; gaps, spoofing, and stale reports are properties of the source, not of the platform. The platform ingests, normalizes, caches, and publishes — it does not verify. Calibration of any forecast that consumes AIS is a separate, gated loop (Gate 1b). We are pre-revenue and building the POC; the stream is wired, and its honesty is that it is a stream of reports, not a stream of facts.