Node connection lifecycle
What this covers
How RainMaker tracks a node’s connection state (online / offline) end-to-end across MQTT broker events, the nodes_online DynamoDB table, and the device shadow. Includes the connect/disconnect/reconnect flows and what happens on internet flicker vs. power outage.
Actors and clocks
Actor |
Role |
|---|---|
Node (FW) |
Maintains MQTT connection. Publishes |
AWS IoT Broker |
Accepts/rejects MQTT connections. Detects ungraceful disconnects via keepalive timeout. Fires |
|
On every connect event (filtered to exclude |
|
On every disconnect event (same filter), invokes the |
|
On each disconnect event, waits |
Device shadow |
Source of truth for |
Clock |
Value |
Effect |
|---|---|---|
MQTT keepalive |
60 s |
Node sends PINGREQ every 60 s. |
Broker keepalive timeout |
~1.5 × keepalive ≈ 90 s |
After this much silence the broker declares the node dead and fires a |
|
10 s |
The grace period before the session is read and the shadow written, giving a reconnecting node (and the corresponding |
Connect (clean)
T=0 Node open TCP, MQTT CONNECT (clientID=X, keepalive=60)
T=0 Broker accept → assign sessionID=S1
fire CONNECTED event for (X, S1, versionNumber)
T=0 Rule PutItem in nodes_online: clientId=X, sessionIdentifier=S1, ...
T=0 Node publish shadow: online=true
T=60 Node PINGREQ
T=120 Node PINGREQ
...
Steady state: nodes_online row exists with S1, shadow shows online=true.
Disconnect (graceful — e.g. reboot, app close)
T=0 Node send MQTT DISCONNECT, close TCP
T=0 Broker fire DISCONNECTED for S1 (reason=CLIENT_INITIATED)
T=0 Lambda fires; sleeps presenceOfflineDelay (10 s)
T=10 Lambda read nodes_online → S1 matches → write shadow online=false
Time from device-off → shadow-reflects-offline: ~10 s.
Disconnect (ungraceful — power outage, hard kill, internet outage with no reconnect)
The TCP connection just dies. The broker has no way to know until keepalive elapses.
T=0 Node power dies. No DISCONNECT sent.
T=0…90 Broker silence. Shadow still shows online=true.
T=90 Broker no PINGREQ for ~1.5× keepalive → fire DISCONNECTED for S1 (reason=KEEPALIVE_TIMEOUT)
T=90 Lambda fires; sleeps 10 s
T=100 Lambda read nodes_online → S1 still matches → write shadow online=false
Time from power-off → shadow-reflects-offline: ~100 s. The 90 s is intrinsic to MQTT — no cloud-side change can detect a silently-dead device faster.
Reconnect (after a brief flicker — Wi-Fi blip < 60 s)
T=0 Node connected on S1, online=true
T=10 Wi-Fi drops briefly. TCP dies but broker hasn't noticed (still inside the 90 s keepalive window).
T=15 Node Wi-Fi back. Tries to publish, sees TCP error, reconnects with same clientID → broker assigns new session S2.
T=15 Broker sees duplicate clientID:
→ fire DISCONNECTED for S1 (reason=DUPLICATE_CLIENTID)
→ fire CONNECTED for S2
→ connected IoT rule begins PutItem(S2) ── async
T=15 Node publish shadow: online=true
T=15.x Lambda fires for S1's DISCONNECTED event; sleeps 10 s.
T=25.x Lambda reads nodes_online.
→ DB has S2 (PutItem landed during the wait), event has S1 → mismatch → DROP
End-state: shadow stays online=true throughout. User sees no flicker.
Known residual cases
Two failure modes can still produce a wrong online=false write. Both are rare. They are documented here as known limitations; the deterministic fix below closes them if they ever become customer-visible.
Case 1 — Scalability
If PutItem(S2) lands more than 10 s after the disconnect event fires (mass-reconnect bursts, DynamoDB throttling, AWS service degradation), the lambda’s DB read at T=10 still sees S1. The session check passes and the lambda writes online=false, clobbering the FW’s fresh online=true. Typical PutItem p99.9 is well under 1 s, so this only triggers under exceptional load — and that load is visible in metrics before the flicker is user-noticeable.
Case 2 — duplicate disconnect for an active session
AWS warns lifecycle events can be duplicated or arrive out of order:
“Lifecycle messages might be sent out of order. You might receive duplicate messages.” Lifecycle events docs
If a duplicate DISCONNECTED event arrives for a session that’s still active, the session check legitimately passes — the session in DB is current, because the device never actually disconnected — and the lambda writes online=false over a healthy device’s shadow. No reconnect or PutItem lag involved.
Deterministic fix (future work)
If either case becomes customer-visible, close both by adding a shadow read + conditional write to the offline path:
1. GetThingShadow → parse current state, version V, metadata.reported.online.timestamp T_shadow
2. If T_shadow ≥ event.Timestamp → drop (FW's online state is at least as fresh as this disconnect)
3. UpdateThingShadow with payload.version = V → write online=false
4. On ConflictException → drop (preserving whoever wrote between the read and the write)
Both version and per-attribute metadata.timestamp are server-set by AWS, so no FW change is required. Cost is one extra GetThingShadow per disconnect that survives the presenceOfflineDelay wait and the session-match check.
The current wait-and-verify pattern (delay, then confirm the tracked session still matches before writing) is itself the AWS-recommended approach for handling lifecycle events — see Lifecycle events docs.
Reconnect (after a long outage — power back, Wi-Fi reset, etc.)
T=0 Node power dies
T=90 Broker KEEPALIVE_TIMEOUT for S1
T=100 Lambda writes shadow online=false
... node remains offline ...
T=600 Node powers back on, MQTT CONNECT → S2
T=600 Rule PutItem(S2)
T=600 Node publish shadow online=true
User-visible transitions: one online → offline (around T=100) and one offline → online (around T=600). No spurious flicker because the gap is much larger than any race window.
End-to-end timing summary
Scenario |
When shadow flips to offline |
Visible flicker? |
|---|---|---|
Graceful disconnect |
~10 s after device closes connection |
None |
Power outage (no reconnect) |
~100 s after power dies (90 s keepalive + 10 s delay) |
None |
Wi-Fi flicker, fast reconnect (< 10 s) |
Never — shadow stays online |
None |
Long outage with eventual reconnect |
offline at ~100 s, online again when device reconnects |
One offline → online pair |