# MQTT alerts feed

> Live NWS hazards over MQTT-on-WebSocket, retained per hazard, tombstoned when they end, and filed by both issuing office and SAME code.

Developers · Reference · https://wxalerts.org/docs/developers/mqtt/alerts/

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](/docs/developers/mqtt/lightning/), on a separate topic tree. To look alerts up
on demand instead of following them live, use the
[alerts REST API](/docs/api/alerts/).

  For Home Assistant, none of this is necessary, because the
  [WxAlerts integration](/docs/integrations/home-assistant/) already speaks this
  feed and turns it into entities. This page is for writing your own client.
  Storm cell alerts for your own places are on separate per account topics,
  described under [MQTT topics](/docs/integrations/home-assistant/mqtt-topics/).

To write a first client, follow
[Subscribe to live alerts over MQTT](/docs/developers/mqtt-quickstart/). For
what keeps one running, see
[Build a reliable MQTT client](/docs/developers/mqtt/best-practices/).

## Connecting

| | |
| --- | --- |
| URL | `wss://mqtt.wxalerts.org/mqtt` |
| Host and port | `mqtt.wxalerts.org`, `443` |
| Transport | MQTT over WebSocket, path `/mqtt`, TLS |
| 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.
A publish from it is dropped without an error and without disconnecting you.
There is no plain MQTT port open to the internet: 1883 and 8883 are closed, so
the WebSocket on 443 is the only way in.

**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 |

QoS is what the ingest publishes at. You receive the lower of that and the QoS
you subscribe with, so subscribe at QoS 1 to get hazards at QoS 1.

The levels are:

- **`{office}`**: the issuing office, like `KMOB`. Alerts that arrive without
  VTEC are given the four-letter office responsible for their zones. Only when
  that cannot be worked out does the sender's name stand in, uppercased with
  non-alphanumerics replaced by underscores (`NWS_MEDFORD_OR`).
- **`{phen}`**: the VTEC phenomenon, so `TO` tornado, `SV` severe thunderstorm,
  `FF` flash flood, and so on. Without VTEC, the slugged event name stands in.
- **`{sig}`**: significance, so `W` warning, `A` watch, `Y` advisory, `S`
  statement. `X` when there is no VTEC to read it from.
- **`{etn}`**: the VTEC event tracking number, zero-padded to four digits.
  Alerts without VTEC use `id` followed 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
```

  Do not subscribe to `alert/#`, `same/#`, or `wxalerts/nws/v1/#` from a client
  application. During a significant outbreak that is the entire national hazard
  set plus every text product in the country. Subscribe to the counties or
  offices you actually care about.

`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.

## Retention

A live hazard is retained, so subscribing delivers it immediately, and every
resubscribe replays the whole retained set for that filter. A hazard
ends with a tombstone, an empty payload retained on its topic. Both are
explained in [Retained alerts, and tombstones](/docs/developers/mqtt/retained-and-tombstones/).

## The payload

```jsonc
{
  "published_at": "2026-08-11T20:15:04.318204+00:00",   // when the ingest sent it
  "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 | Unknown
  "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": ["api", "nwws", "emwin"]   // which feeds carried it
}
```

Timestamps are ISO 8601 with an offset. `published_at` is stamped as the
message leaves the ingest, so it is the one field you can measure the broker
hop against; `issued_at` is when NWS wrote the product. `geometry` is a GeoJSON
`Polygon` or `MultiPolygon` in WGS84, or `null` for a hazard that has no shape.

Notes on individual fields:

- `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. `ends` is the end.
- `action` is the latest VTEC action, not the state. A hazard that has been
  cancelled for some of its counties stays `active`, on its remaining counties,
  with `action` set to `CAN`. `status` is the state.
- `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.
  Geometry is simplified for the wire.
- 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. `id` is the same on every copy.

## 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 `published_at`, `wmo_id`, `awips_id`, `office`, `issued_at`,
`source`, `hash` and the raw `body`. `{hash}` in the topic is the first 16
characters of `hash`. `awips_id` is `null` for products that have none, and
`source` is the feed that delivered the product first, `nwws` or `emwin`. 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.

## Broker limits

| limit | value |
| --- | --- |
| subscriptions per client | 20. The 21st subscribe is refused with a failure code in the SUBACK while the connection stays up |
| new WebSocket connections | at most 50 a second across all clients |
