Every NWS hazard this system ingests is published to a public broker as it arrives, and stays there, retained, for as long as it is live. Subscribing replays everything currently in effect for your filter, so a client that has been offline comes back up knowing the current state of the world instead of waiting for the next statement.
This is the same broker that carries the lightning feed, on a separate topic tree.
Connecting
| URL | wss://mqtt.wxalerts.org/mqtt |
| Transport | MQTT over WebSocket |
| Username | wxalerts |
| Password | wxalerts |
The credentials are published on purpose. The account is subscribe-only and the broker’s ACL denies publishing on every topic, so there is nothing to protect.
Your client ID must be unique. Two clients sharing one will kick each other off in a loop. Derive it from something stable and per-installation rather than hardcoding a constant.
The topic tree
Every hazard is announced twice: once under the office that issued it, and once per county it covers.
| Topic | Retained | QoS | What it carries |
|---|---|---|---|
wxalerts/nws/v1/alert/{office}/{phen}/{sig}/{etn} |
yes | 1 | one live hazard, by issuing office |
wxalerts/nws/v1/same/{same}/{etn} |
yes | 1 | the same hazard, by county |
wxalerts/nws/v1/alert/{office}/Messages/{hash} |
1 hour | 0 | raw text products |
The levels are:
{office}: the issuing WFO, likeKMOB. Alerts that arrive without VTEC carry the sender’s name instead, uppercased with non-alphanumerics replaced by underscores (NWS_MEDFORD_OR).{phen}: the VTEC phenomenon, soTOtornado,SVsevere thunderstorm,FFflash flood, and so on. Without VTEC, the slugged event name stands in.{sig}: significance, soWwarning,Awatch,Yadvisory,Sstatement.Xwhen there is no VTEC to read it from.{etn}: the VTEC event tracking number, zero-padded to four digits. Alerts without VTEC useidfollowed by the alert’s own id.
The ETN is in the topic because the message is retained and one office can have
two tornado warnings live at once. Without it they would share
alert/KMOB/TO/W and the second would erase the first.
Subscribing
Pick the tree that matches how you think about coverage. By county, the way a weather radio works:
wxalerts/nws/v1/same/012113/# one county, every hazard
Or by office and phenomenon:
wxalerts/nws/v1/alert/KMOB/# everything one office issues
wxalerts/nws/v1/alert/+/TO/W/+ every tornado warning in the country
wxalerts/nws/v1/alert/KMOB/TO/W/# one office's tornado warnings
alert/# also matches text products, because messages are filed under the
office that issued them. The two stay separable by shape: a hazard is the office
plus three levels (alert/+/+/+/+), a message is the office plus two
(alert/+/Messages/+). The shapes never collide.
Retained alerts, and tombstones
A live hazard is retained, so subscribing delivers it immediately.
A hazard ends by way of a tombstone: an empty payload, retained, on the topic it lived on. Drop the alert when one arrives.
This matters more than it sounds. Expiring alerts on local clock arithmetic
means a cancelled warning hangs around until its original end time, and a client
whose clock has drifted gets it wrong in both directions. Trust status and the
tombstones over anything you compute yourself.
The payload
{
"id": 1234,
"vtec": "KMOB.TO.W.0012.2026", // null for alerts without VTEC
"event": "Tornado Warning",
"office": "KMOB",
"phenomena": "TO", // null without VTEC
"significance": "W", // W warning, A watch, Y advisory, S statement
"etn": 12,
"action": "NEW", // NEW CON EXT EXA EXB CAN EXP UPG
"status": "active", // active | expired | cancelled
"severity": "Extreme", // Extreme | Severe | Moderate | Minor
"urgency": "Immediate",
"certainty": "Observed",
"issued_at": "2026-08-11T20:15:00+00:00",
"onset": "2026-08-11T20:15:00+00:00",
"expires": "2026-08-11T20:45:00+00:00",
"ends": "2026-08-11T20:45:00+00:00",
"ugc": ["FLC113", "ALC003"],
"same": ["012113", "001003"],
"headline": "TORNADO WARNING IN EFFECT UNTIL 345 PM CDT",
"description": "At 315 PM CDT, a severe thunderstorm capable of…",
"instruction": "TAKE COVER NOW! Move to a basement…",
"geometry": { "type": "MultiPolygon", "coordinates": [] }, // GeoJSON, WGS84
"geometry_source": "polygon", // polygon | ugc | none
"sources": ["emwin", "api"]
}
Timestamps are ISO 8601 with an offset. geometry is GeoJSON in WGS84, or
null for a hazard that has no shape.
Reading the feed
import json
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
# One county: Mobile, Alabama.
client.subscribe("wxalerts/nws/v1/same/012113/#", qos=1)
def on_message(client, userdata, msg):
if not msg.payload:
print("ended:", msg.topic) # tombstone, not JSON
return
alert = json.loads(msg.payload)
print(alert["event"], alert["headline"], alert["ends"])
client = mqtt.Client(client_id="my-unique-client", transport="websockets")
client.username_pw_set("wxalerts", "wxalerts")
client.tls_set()
client.on_connect = on_connect
client.on_message = on_message
client.connect("mqtt.wxalerts.org", 443)
client.loop_forever()
Text products
Raw products are published under the office that issued them, keyed by a content hash so the same product arriving on two feeds lands on one topic instead of appearing twice:
wxalerts/nws/v1/alert/{office}/Messages/{hash}
They carry wmo_id, awips_id, office, issued_at, source, hash and the
raw body. They are retained for about an hour and then tombstoned, so a
subscriber joining mid-hour sees what an office has put out recently.
Surface observation collectives are not published here. At roughly 350 bulletins an hour they would swamp the tree with instrument data nobody subscribed to it for.
Things that will bite you
expires is not the end of the hazard. It is CAP’s “expect an update by”
deadline and routinely sits in the past on a live alert. Use ends, and trust
status and tombstones over your own arithmetic.
One hazard arrives more than once. A warning covering three counties is
published on three same/ topics plus its office topic, with an identical
payload each time. If your filter overlaps, dedupe on id.
There is a 20-subscription cap per client. A subscription per
county-per-phenomenon will hit it. Subscribe to same/{code}/# and filter in
your own code.
Every resubscribe replays the whole retained set. A hot reconnect loop is a bandwidth amplifier pointed at the broker. Use exponential backoff, and note that holding an MQTT session open without reading the socket does not detect a dead connection.
geometry_source tells you how precise the shape is. polygon is a tight
storm-based warning polygon; ugc is a coarser union of county boundaries.
Worth styling differently. Geometry is simplified for the wire.
