Error Handling & Reconnection
:::info Status
Implemented. Source: docs/user-device-mqtt-contract.md §3, §8, §8a, §9a, docs/transmitter-mqtt-contract.md §2, §4.
:::
Reconnection
Both device types: exponential backoff 1s → 60s max, jittered. The audio uplink WebSocket uses the same posture.
flapping_detect on the broker bans a client for 5 minutes after 10
connects in a minute — jittered backoff means a healthy unit recovering
from an outage never trips it; a unit stuck in a connect loop does, and at
fleet scale that's indistinguishable from an attack. See
Device Deployment — listener rate limits.
An MQTT disconnect must never stop audio. Both contracts require the audio pipeline and the MQTT client to run independently — see Events — MQTT down, audio still playing.
Close codes (audio uplink WebSocket)
| Close code | Reason |
|---|---|
4404 | room_id isn't a known room |
4401 | Unknown device, wrong secret, or the device is not ACTIVE-and-bound |
A 4401/4404 close is terminal for that connection attempt — not
something to retry immediately in a tight loop. See
Transmitter Audio Uplink.
Session semantics govern what "offline" means
| Transmitter | Stream player | |
|---|---|---|
| Clean session | true | clean_start=true, Session-Expiry-Interval=0 |
cmd while offline | dropped (no persistent session to queue in) | dropped, deliberately |
A play missed during an outage | never delivered late | never delivered late |
The stream player contract states this most explicitly: a persistent
session that queued commands was considered and rejected, because a
queued play delivered late means the unit broadcasts a stale azaan at
the wrong hour. Configuration survives disconnection through retained
messages, not the session — a unit off for a month reconnects and gets
current desired state in one message. The broker never holds undelivered
azaan; a missed trigger is recorded not_confirmed, never replayed.
Idempotency and replay — duplicates are guaranteed, not a failure mode
QoS 1, retained messages, a backend outbox that retries, and residential links that drop mid-delivery together guarantee every command and config arrives more than once. The device must handle that; the backend cannot do it on the device's behalf.
- Duplicate commands — keep a seen-set of the last 100
req_ids (or 1 hour, whichever is larger). Already seen → re-send the original ack, do not execute again. Executingdeauthorizetwice is harmless;playtwice mid-azaan is audible; a redeliveredunbind_mosqueafter a rebind would silently undo the rebind. - Expired commands —
expires_atpassed → ackresult:"expired"and do nothing. Aplayredelivered twenty minutes after Maghrib must not play. - Config versions are monotonic:
The last rule matters most: on reconnect a device can receive a retained config older than what it holds (e.g. a rebind published while it was offline races a broker redelivery). Applying it would silently move the unit back to its previous mosque.incoming config_version > stored → apply, persist, ackincoming config_version == stored → ack, do not re-applyincoming config_version < stored → IGNORE, ack ← stale retained message
No trusted clock — time-sensitive commands before NTP sync
A device with no battery-backed RTC has a wrong clock after every power
loss until NTP completes. In that window expires_at can't be evaluated,
and a replayed old play would otherwise execute:
before NTP sync: discard time-sensitive commands → ack result:"expired",
data.reason:"clock_not_synced"; quiet_hours treated as disabled
after NTP sync: normal evaluation
Rejecting a real azaan in the seconds after a reboot is a smaller fault than playing a replayed one at the wrong hour. See Device Lifecycle — power-loss convergence for the full boot sequence this fits into.
Broker outage
What stops and what keeps working during a broker outage — and why a
single-node broker is defensible as a first stage — is covered in
Monitoring — what a broker outage costs.
In short: scheduled azaan keeps playing from cached config until the
entitlement lease (stream player only)
expires; Bluetooth and app installs are unaffected; live play commands,
status, and heartbeat stop until the broker recovers.
Crash-loop containment (stream player)
More than 5 reboots in 10 minutes → back off to a 5-minute retry interval
before reconnecting, and publish CONFIG_ERROR with
detail.reason:"crash_loop" once able. A fleet crash-looping after a bad
config or OTA is otherwise indistinguishable from a reconnect storm and
does the same damage to the broker.