Skip to content

Commit a9eba67

Browse files
authored
fix(mqtt): raise QoS1 retransmit timeout to stop duplicate /status storms
esp-mqtt's default message_retransmit_timeout is 1000 ms: any unacked QoS 1 PUBLISH is resent (byte-identical, DUP=1) every second until the PUBACK arrives or the outbox entry expires (30 s). Status messages are the only QoS 1 publishes; on a congested or recovering uplink where broker acks take several seconds, each 5-minute /status was delivered ~6 times, ~1 s apart, as exact copies (same timestamp and stats). Downstream observers flagged excessive_packet_copies and at least one broker treats it as abuse. Expose message_retransmit_timeout via PsychicMqttClient and set it to 15 s in optimizeMqttClientConfig: one retry still fits inside the 30 s outbox expiry, preserving at-least-once delivery while capping duplicates at one. /packets paths are QoS 0 and were never affected.
2 parents e8d41a6 + 4cff796 commit a9eba67

3 files changed

Lines changed: 31 additions & 0 deletions

File tree

lib/PsychicMqttClient/src/PsychicMqttClient.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ PsychicMqttClient &PsychicMqttClient::setAutoReconnect(bool reconnect)
5959
return *this;
6060
}
6161

62+
PsychicMqttClient &PsychicMqttClient::setMessageRetransmitTimeout(int timeoutMs)
63+
{
64+
#if ESP_IDF_VERSION_MAJOR == 5
65+
_mqtt_cfg.session.message_retransmit_timeout = timeoutMs;
66+
#else
67+
_mqtt_cfg.message_retransmit_timeout = timeoutMs;
68+
#endif
69+
_config_dirty = true;
70+
return *this;
71+
}
72+
6273
PsychicMqttClient &PsychicMqttClient::setClientId(const char *clientId)
6374
{
6475
#if ESP_IDF_VERSION_MAJOR == 5

lib/PsychicMqttClient/src/PsychicMqttClient.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ class PsychicMqttClient
126126
*/
127127
PsychicMqttClient &setAutoReconnect(bool reconnect = true);
128128

129+
/**
130+
* @brief Sets the retransmit timeout for unacknowledged QoS 1/2 messages.
131+
* esp-mqtt resends an unacked PUBLISH (DUP flag set) every time this
132+
* timeout elapses, so a value shorter than the broker's ack latency
133+
* produces byte-identical duplicates on the wire.
134+
*
135+
* @param timeoutMs Retransmit timeout in milliseconds. esp-mqtt's default is 1000.
136+
* @return A reference to the PsychicMqttClient instance.
137+
*/
138+
PsychicMqttClient &setMessageRetransmitTimeout(int timeoutMs);
139+
129140
/**
130141
* @brief Sets the client ID for the MQTT connection.
131142
*

src/helpers/bridges/MQTTBridge.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3323,6 +3323,15 @@ void MQTTBridge::optimizeMqttClientConfig(PsychicMqttClient* client, bool needs_
33233323
client->setKeepAlive(75);
33243324
#endif
33253325

3326+
// QoS 1 retransmit timeout for unacked PUBLISHes (status messages). esp-mqtt's
3327+
// 1000 ms default resends a byte-identical duplicate every second whenever the
3328+
// broker's PUBACK takes >1s — on a congested or recovering uplink this floods
3329+
// subscribers with exact copies of one /status message (observed 6 copies ~1s
3330+
// apart after an ISP outage; brokers may drop the session as spam). 15s allows
3331+
// one retry before the outbox entry expires (esp-mqtt outbox expiry is 30s),
3332+
// preserving at-least-once delivery while capping duplicates at one.
3333+
client->setMessageRetransmitTimeout(15000);
3334+
33263335
// Buffer sizing: 896 is the minimum safe size for JWT clients (CONNECT + 768-byte JWT).
33273336
// On PSRAM boards, use a uniform size to reduce fragmentation from mixed allocations.
33283337
// On non-PSRAM boards, use smaller buffers for non-JWT slots to reduce heap usage and

0 commit comments

Comments
 (0)