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.
This commit is contained in:
Adam Gessaman authored and GitHub committed 2026-07-10 08:12:02 -07:00
commit a9eba67dd2
3 files changed
+31

No files matched your search

@@ -59,6 +59,17 @@ PsychicMqttClient &PsychicMqttClient::setAutoReconnect(bool reconnect)
return *this;
}
PsychicMqttClient &PsychicMqttClient::setMessageRetransmitTimeout(int timeoutMs)
{
#if ESP_IDF_VERSION_MAJOR == 5
_mqtt_cfg.session.message_retransmit_timeout = timeoutMs;
#else
_mqtt_cfg.message_retransmit_timeout = timeoutMs;
#endif
_config_dirty = true;
return *this;
}
PsychicMqttClient &PsychicMqttClient::setClientId(const char *clientId)
{
#if ESP_IDF_VERSION_MAJOR == 5
@@ -126,6 +126,17 @@ public:
*/
PsychicMqttClient &setAutoReconnect(bool reconnect = true);
/**
* @brief Sets the retransmit timeout for unacknowledged QoS 1/2 messages.
* esp-mqtt resends an unacked PUBLISH (DUP flag set) every time this
* timeout elapses, so a value shorter than the broker's ack latency
* produces byte-identical duplicates on the wire.
*
* @param timeoutMs Retransmit timeout in milliseconds. esp-mqtt's default is 1000.
* @return A reference to the PsychicMqttClient instance.
*/
PsychicMqttClient &setMessageRetransmitTimeout(int timeoutMs);
/**
* @brief Sets the client ID for the MQTT connection.
*
+9
View File
@@ -3323,6 +3323,15 @@ void MQTTBridge::optimizeMqttClientConfig(PsychicMqttClient* client, bool needs_
client->setKeepAlive(75);
#endif
// QoS 1 retransmit timeout for unacked PUBLISHes (status messages). esp-mqtt's
// 1000 ms default resends a byte-identical duplicate every second whenever the
// broker's PUBACK takes >1s — on a congested or recovering uplink this floods
// subscribers with exact copies of one /status message (observed 6 copies ~1s
// apart after an ISP outage; brokers may drop the session as spam). 15s allows
// one retry before the outbox entry expires (esp-mqtt outbox expiry is 30s),
// preserving at-least-once delivery while capping duplicates at one.
client->setMessageRetransmitTimeout(15000);
// Buffer sizing: 896 is the minimum safe size for JWT clients (CONNECT + 768-byte JWT).
// On PSRAM boards, use a uniform size to reduce fragmentation from mixed allocations.
// On non-PSRAM boards, use smaller buffers for non-JWT slots to reduce heap usage and