# MQTT input stops or faults

> An MQTT input datasource goes idle mid-stream, predictions read NaN, or the MQTT output sink logs "transport not connected" — five distinct root causes and how to tell them apart.

## Symptom

An MQTT-backed datasource was streaming, and something breaks:

- **Inference goes from running to idle/faulted** on its own — no one pressed Stop.
- **Predictions come back as `NaN`**, or the dashboard shows nothing plausible.
- **The MQTT output sink reports write failures**, with a reason like
  `transport: not connected`.
- **A tag mapping is rejected at save time** with a `tag_outside_subscription_*` message.
- **Enable, or Test Connection, fails outright** before anything ever streamed.

MQTT is a **push** protocol — inference **pulls** one sample per tick from a cache the
adapter fills as messages arrive. Every cause below is really the same question from a
different angle: *did the cache get a usable value recently enough?*

## Confirm

Five distinct failure modes share this symptom set. Grep the backend log to tell them
apart — all examples below show only error codes/reasons, never payload contents or
credentials.

```bash
docker compose -f docker-compose.release.yml logs backend | grep -iE "MqttAdapter|ChannelReadException|channel .* read failed|tag stale|disconnected|not connected"
```

1. **Stale topic (slow or dead publisher).** A warning fires before the fault:

   ```
   MqttAdapter tag stale: tag=<topic> age=<age> staleAfter=<window>
   Channel <index> (<tag>) read failed — stopping inference
   Inference fault: <reason>
   ```

   The cache expiry window is `clamp(50 / SamplingHz, 5s, 60s)` — **5 seconds** at the
   default 10 Hz. A topic publishing slower than that window ages out and raises
   `ChannelReadException`, which the runtime treats the same as a dead sensor: it stops
   inference rather than feed the model a frozen value.

2. **`NaN` / `Infinity` sentinel.** The MQTT payload parser only accepts *finite*
   numbers — `NaN`, `Infinity`, `-Infinity`, and an overflowing literal like `1e400` are
   all rejected before they ever reach the cache. A device that publishes a "no
   measurement" sentinel this way never refreshes its cached value, so it ages out and
   fails **exactly the same way** as cause 1 above — same log lines, same
   `ChannelReadException`. There is no separate "NaN reached the model" failure mode by
   design; that would silently poison every downstream prediction instead.

3. **Client-id eviction.** Look for:

   ```
   MqttAdapter disconnected (SessionTakenOver); reconnecting
   ```

   or, on the output side, a write failing with:

   ```
   transport: not connected
   ```

   MQTT brokers evict the *incumbent* connection when a second client connects with the
   same client id. On a current build this now happens only when: **two datasources of
   the same role** (two inputs, or two outputs) share one explicit **Client ID**, or an
   **external MQTT client** reuses a datasource's configured id.

   
**Input/output pairs on one config no longer collide (v1.28.0)**

   A configured **Client ID** is automatically role-suffixed at connect time
   (`<id>-in` for the input adapter, `<id>-out` for the output sink) — this applies to
   every saved config, including an older one still carrying the shared `aiboard`
   default, so an MQTT input and output built from the *same* datasource config can no
   longer evict each other. If you still see this on an in/out pair, you're on a build
   older than v1.28.0 — upgrade rather than hand-editing the id.
   

4. **Tag outside the subscribed Base Topic.** Saving a tag mapping (or enabling the
   datasource) fails immediately with:

   ```
   tag_outside_subscription_<tagRef>
   ```

   The input adapter subscribes only to its mapped tags (or the configured **Base
   Topic** wildcard) — a topic path outside that scope is rejected at save time instead
   of silently never producing a reading.

5. **Socket-level connect failure.** Test Connection or Enable fails before any
   streaming starts, with one of:

   connect_failed unreachable / refused / DNS failure &nbsp;
   timeout the connect attempt was cancelled before finishing

   Backend log corroborates with the elapsed time and exception type:

   ```
   MqttAdapter connect failed: broker=<broker> elapsedMs=<ms> errorType=<type>
   ```

   
**The Docker localhost trap**

   Inside a container, `localhost` means **the backend container itself**, not your
   desktop or the Docker host. A broker that a desktop MQTT client reaches fine at
   `localhost:1883` is unreachable from AIBOARD at the same address — use the broker's
   service/container name (e.g. `mqtt://mosquitto:8884` on a Docker network) or
   `host.docker.internal` instead.
   

## Fix

1. **Stale topic / NaN sentinel (causes 1–2).** Fix the publisher's cadence or stop it
   from publishing a non-finite sentinel, then re-enable the datasource — a fault tears
   the runtime down to idle, so recovery is not automatic:
   - Confirm the mapped topic's real publish interval and compare it against the stale
     window at the datasource's configured **Sampling Hz**.
   - If the topic legitimately reports "no measurement", switch the device/gateway to
     simply **not publish** during that period rather than sending `NaN`/`Infinity` —
     both are treated identically (a quiet topic), so there's no advantage to sending
     the sentinel.
   - Re-enable the datasource (reloads the model, reconnects the adapter), then press
     **Start**.

2. **Client-id eviction (cause 3).** Give each same-role datasource (two inputs, or two
   outputs) sharing an explicit **Client ID** a distinct value, or clear the field so
   MQTTnet auto-generates a unique id per side. If an external client is reusing your
   datasource's id, change one of them. This does **not** apply to an input/output pair
   built from the same config — the v1.28.0 role suffix (`-in`/`-out`) already keeps
   those from colliding, with no edit needed.

3. **Tag outside subscription (cause 4).** Either widen the datasource's **Base Topic**
   to cover the tag's topic path, or fix the tag's topic path to fall inside the current
   Base Topic. Re-save the mapping.

4. **Socket failure (cause 5).** Verify host, port, and network reachability from
   *inside* the backend container — not from your desktop. Swap `localhost` for the
   broker's container/service name (or `host.docker.internal` from a container to the
   Docker host), then retry **Test Connection**.

## Prevent

- **Check every mapped topic's publish interval against the stale window** before
  enabling a new MQTT input — lower **Sampling Hz** to widen tolerance (1 Hz → 50s) if a
  topic legitimately publishes slowly.
- **Never reuse one explicit Client ID across two datasources of the same role** (two
  inputs, or two outputs), and don't reuse a datasource's Client ID for an external MQTT
  client. An input/output pair on the same config is already safe — v1.28.0 auto-suffixes
  each side (`-in`/`-out`).
- **Scope the Base Topic tightly** to the tags you actually map, and map tags whose
  topic paths already fall inside it.
- **Don't encode "no data" as `NaN`/`Infinity` on the wire.** A device that goes quiet
  when it has nothing to report behaves identically and is easier to reason about than
  a sentinel value.
- **Test Connection from the box's perspective**, not your workstation's — a broker
  reachable from your desktop over `localhost` may not be reachable from the backend
  container at all.

## Related

- [Notifications](/operate/notifications/) — the involuntary-stop alert and output
  write-failure notifications that surface these faults in the UI.
- [Connecting Input Datasources](/configure/input-datasources/) — staleness window,
  reconnect behaviour, and non-finite rejection in full.
- [Output Datasources](/configure/output-datasources/) — MQTT publish QoS, client-id
  suffixing, and Test Connection diagnostics.
- [Datasource down or faulted](/troubleshooting/datasource-down-or-faulted/) — the
  general Enable/fault runbook this page specializes for MQTT.
