Client Keep Alive PINGREQ Scheduling Is Not Built Into the Core Client
Summary
MQTT 3.1.1 requires the Client to ensure that the interval between MQTT Control Packets sent to the Server does not exceed the negotiated Keep Alive value. If the Client has no other MQTT Control Packet to send before that interval expires, it must send a PINGREQ.
wolfMQTT supports the building blocks for this behavior: it encodes the Keep Alive field in CONNECT, provides MqttClient_Ping() / MqttClient_Ping_ex(), sends a valid PINGREQ, and waits for PINGRESP. However, the core client receive/wait loop does not automatically track the elapsed time since the last outbound Control Packet and does not automatically send PINGREQ before the Keep Alive interval expires.
The application or example code must schedule the ping itself. Therefore this is best classified as partial satisfaction, not as a complete absence of Keep Alive support.
MQTT 3.1.1 Requirement
Source: OASIS MQTT Version 3.1.1, online HTML, Section 3.1.2, Keep Alive clauses MQTT-3.1.2-23 and MQTT-3.1.2-24.
Relevant section:
- Section 3.1.2, CONNECT Variable Header, Keep Alive.
- Normative statement
[MQTT-3.1.2-23].
- Related server-side timeout statement
[MQTT-3.1.2-24].
Short original English excerpts:
"does not exceed the Keep Alive value"
"Client MUST send a PINGREQ Packet"
"one and a half times"
Detailed English description of the standard requirement:
The Keep Alive field is a 16-bit value measured in seconds. It defines the maximum permitted interval between the moment the Client finishes transmitting one MQTT Control Packet and the moment the Client starts transmitting the next MQTT Control Packet. This is a Client responsibility. If the Client is otherwise idle and has no normal application traffic or protocol packet to send, it must send a `PINGREQ` before the Keep Alive interval is exceeded.
The Client is allowed to send `PINGREQ` earlier than the Keep Alive deadline. This means a conforming implementation can use an application-level timer shorter than the Keep Alive value. What it must not do is wait until after the Keep Alive interval has already been exceeded and then treat that late ping as satisfying the requirement.
On the Server side, if the Keep Alive value is non-zero and the Server receives no Control Packet from the Client within 1.5 times the Keep Alive period, the Server must disconnect the Network Connection. That server-side rule is separate from the Client's earlier responsibility to avoid exceeding the Keep Alive interval in the first place.
Expected Client Behavior
For a connected Client with non-zero Keep Alive:
| Situation |
Expected behavior |
| Normal outbound MQTT Control Packets are sent within the Keep Alive interval |
No extra PINGREQ is required |
| No outbound MQTT Control Packet has been sent and the Keep Alive deadline is approaching |
Client sends PINGREQ before the interval is exceeded |
Client sends PINGREQ and does not receive PINGRESP in a reasonable time |
Client should close the Network Connection |
| Keep Alive is zero |
The Keep Alive mechanism is disabled by the negotiated value |
The important timing point is that PINGREQ is a preventive keepalive packet. It should be scheduled before the negotiated interval is exceeded, not only after the MQTT Keep Alive requirement has already been violated.
Code Evidence
1. CONNECT carries the Keep Alive value
File: wolfMQTT-master/src/mqtt_packet.c
Function: MqttEncode_Connect()
Relevant line: 888.
The encoder writes mc_connect->keep_alive_sec into the CONNECT variable header:
MqttEncode_Num((byte*)&packet.keep_alive, mc_connect->keep_alive_sec)
The decoder also reads the field into mc_connect->keep_alive_sec at line 1005.
This part satisfies the wire-format requirement for carrying the Keep Alive value.
2. The client provides an explicit Ping API
File: wolfMQTT-master/src/mqtt_client.c
Function: MqttClient_Ping_ex()
Relevant lines: 2572-2641.
MqttClient_Ping_ex() performs the actual keepalive ping operation:
- It calls
MqttEncode_Ping() to encode a PINGREQ.
- It writes the encoded packet using
MqttPacket_Write().
- It waits for
MQTT_PACKET_TYPE_PING_RESP using MqttClient_WaitType().
The wrapper MqttClient_Ping() at line 2661 calls MqttClient_Ping_ex() using the client's internal ping object.
This means wolfMQTT can send a correct PINGREQ, but only when the caller explicitly invokes the ping API.
3. PINGREQ encoding is correct
File: wolfMQTT-master/src/mqtt_packet.c
Function: MqttEncode_Ping()
Relevant lines: 2355-2366.
The encoder builds a PINGREQ packet with packet type MQTT_PACKET_TYPE_PING_REQ and no variable header or payload. The resulting fixed header is the expected MQTT bytes:
This confirms that the missing part is not packet encoding. The missing part is automatic scheduling.
4. The core wait API does not schedule Keep Alive
File: wolfMQTT-master/src/mqtt_client.c
Functions:
MqttClient_WaitMessage_ex(), line 2874.
MqttClient_WaitMessage(), line 2880.
MqttClient_WaitMessage_ex() simply calls:
MqttClient_WaitType(client, msg, MQTT_PACKET_TYPE_ANY, 0, timeout_ms)
There is no check of the negotiated Keep Alive value, no tracking of the last outbound MQTT Control Packet time, and no automatic call to MqttClient_Ping() or MqttClient_Ping_ex() from this wait path.
5. Example code performs Keep Alive scheduling at application level
File: wolfMQTT-master/examples/mqttclient/mqttclient.c
Relevant lines: 554-600.
The example read loop calls:
MqttClient_WaitMessage(&mqttCtx->client, mqttCtx->cmd_timeout_ms)
When that application-defined wait period returns MQTT_CODE_ERROR_TIMEOUT, the example prints a keepalive message and explicitly calls:
MqttClient_Ping_ex(&mqttCtx->client, &mqttCtx->ping)
This demonstrates the intended usage pattern in the examples: the application chooses a wait interval and calls the Ping API itself. This can be compliant if the application timeout is short enough to send PINGREQ before the MQTT Keep Alive interval is exceeded. It is not an automatic guarantee provided by the core client library.
6. Broker-side Keep Alive timeout is implemented separately
File: wolfMQTT-master/src/mqtt_broker.c
Relevant lines:
2737: broker stores mc.keep_alive_sec.
3577: broker responds to PINGREQ with PINGRESP.
3607-3611: broker checks for 1.5 times Keep Alive timeout.
The broker/server side therefore has logic for the [MQTT-3.1.2-24] timeout rule. This does not remove the Client-side scheduling gap described in this document.
Local Probe Result
A temporary local probe was compiled against the wolfMQTT source tree to check whether MqttClient_WaitMessage() automatically sends PINGREQ.
Probe design:
- The mock network
read callback always returned MQTT_CODE_ERROR_TIMEOUT.
- The mock network
write callback counted writes and detected the PINGREQ bytes C0 00.
- The probe first called
MqttClient_WaitMessage(&client, 1).
- The probe then called
MqttClient_Ping(&client) as a positive control.
Observed output:
wait_rc=-7 read_calls=1 write_calls=0 pingreq_writes=0
manual_ping_rc=-7 read_calls=1 write_calls=1 pingreq_writes=1 first_write=C000
Interpretation:
| Call |
Result |
MqttClient_WaitMessage() |
Timed out, but performed zero writes and sent no PINGREQ |
MqttClient_Ping() |
Wrote one packet, detected as PINGREQ bytes C0 00 |
The probe confirms that the core wait path does not automatically send Keep Alive pings. The temporary probe source and executable were removed after the test.
Why This Is an Inconsistency
The MQTT standard assigns the Keep Alive sending responsibility to the Client. For a complete client behavior, the implementation must ensure that the Client sends some MQTT Control Packet before the Keep Alive interval is exceeded. If no other Control Packet is sent, that packet must be PINGREQ.
wolfMQTT's core client library does not enforce this timing rule by itself:
- It can advertise a Keep Alive value in CONNECT.
- It can send
PINGREQ and wait for PINGRESP.
- It does not internally store or enforce a last-outbound-Control-Packet deadline.
- It does not automatically call the Ping API from
MqttClient_WaitMessage() when the connection is idle.
- Example programs implement the scheduling externally by reacting to an application-level timeout.
The mismatch is therefore:
| Standard expectation |
wolfMQTT core client behavior |
| Client ensures outbound Control Packet interval does not exceed Keep Alive |
Core client exposes APIs, but does not automatically enforce the interval |
Idle Client sends PINGREQ before the Keep Alive interval is exceeded |
Application code must decide when to call MqttClient_Ping() / MqttClient_Ping_ex() |
This is a real gap if the evaluated target is the core wolfMQTT client library itself. It is less severe than a malformed-packet acceptance bug because wolfMQTT provides the required API primitives and the examples show how an application can schedule pings correctly.
Reason:
The Keep Alive scheduling concerns describe the same underlying issue. wolfMQTT has Keep Alive support at the packet/API level, but the core client does not automatically maintain the Client-side Keep Alive obligation. A conforming application must schedule MqttClient_Ping() / MqttClient_Ping_ex() before the negotiated Keep Alive interval expires.
Client Keep Alive PINGREQ Scheduling Is Not Built Into the Core Client
Summary
MQTT 3.1.1 requires the Client to ensure that the interval between MQTT Control Packets sent to the Server does not exceed the negotiated Keep Alive value. If the Client has no other MQTT Control Packet to send before that interval expires, it must send a
PINGREQ.wolfMQTT supports the building blocks for this behavior: it encodes the Keep Alive field in
CONNECT, providesMqttClient_Ping()/MqttClient_Ping_ex(), sends a validPINGREQ, and waits forPINGRESP. However, the core client receive/wait loop does not automatically track the elapsed time since the last outbound Control Packet and does not automatically sendPINGREQbefore the Keep Alive interval expires.The application or example code must schedule the ping itself. Therefore this is best classified as partial satisfaction, not as a complete absence of Keep Alive support.
MQTT 3.1.1 Requirement
Source: OASIS MQTT Version 3.1.1, online HTML, Section 3.1.2, Keep Alive clauses MQTT-3.1.2-23 and MQTT-3.1.2-24.
Relevant section:
[MQTT-3.1.2-23].[MQTT-3.1.2-24].Short original English excerpts:
Detailed English description of the standard requirement:
Expected Client Behavior
For a connected Client with non-zero Keep Alive:
PINGREQis requiredPINGREQbefore the interval is exceededPINGREQand does not receivePINGRESPin a reasonable timeThe important timing point is that
PINGREQis a preventive keepalive packet. It should be scheduled before the negotiated interval is exceeded, not only after the MQTT Keep Alive requirement has already been violated.Code Evidence
1. CONNECT carries the Keep Alive value
File:
wolfMQTT-master/src/mqtt_packet.cFunction:
MqttEncode_Connect()Relevant line:
888.The encoder writes
mc_connect->keep_alive_secinto the CONNECT variable header:The decoder also reads the field into
mc_connect->keep_alive_secat line1005.This part satisfies the wire-format requirement for carrying the Keep Alive value.
2. The client provides an explicit Ping API
File:
wolfMQTT-master/src/mqtt_client.cFunction:
MqttClient_Ping_ex()Relevant lines:
2572-2641.MqttClient_Ping_ex()performs the actual keepalive ping operation:MqttEncode_Ping()to encode aPINGREQ.MqttPacket_Write().MQTT_PACKET_TYPE_PING_RESPusingMqttClient_WaitType().The wrapper
MqttClient_Ping()at line2661callsMqttClient_Ping_ex()using the client's internal ping object.This means wolfMQTT can send a correct
PINGREQ, but only when the caller explicitly invokes the ping API.3. PINGREQ encoding is correct
File:
wolfMQTT-master/src/mqtt_packet.cFunction:
MqttEncode_Ping()Relevant lines:
2355-2366.The encoder builds a
PINGREQpacket with packet typeMQTT_PACKET_TYPE_PING_REQand no variable header or payload. The resulting fixed header is the expected MQTT bytes:This confirms that the missing part is not packet encoding. The missing part is automatic scheduling.
4. The core wait API does not schedule Keep Alive
File:
wolfMQTT-master/src/mqtt_client.cFunctions:
MqttClient_WaitMessage_ex(), line2874.MqttClient_WaitMessage(), line2880.MqttClient_WaitMessage_ex()simply calls:There is no check of the negotiated Keep Alive value, no tracking of the last outbound MQTT Control Packet time, and no automatic call to
MqttClient_Ping()orMqttClient_Ping_ex()from this wait path.5. Example code performs Keep Alive scheduling at application level
File:
wolfMQTT-master/examples/mqttclient/mqttclient.cRelevant lines:
554-600.The example read loop calls:
When that application-defined wait period returns
MQTT_CODE_ERROR_TIMEOUT, the example prints a keepalive message and explicitly calls:This demonstrates the intended usage pattern in the examples: the application chooses a wait interval and calls the Ping API itself. This can be compliant if the application timeout is short enough to send
PINGREQbefore the MQTT Keep Alive interval is exceeded. It is not an automatic guarantee provided by the core client library.6. Broker-side Keep Alive timeout is implemented separately
File:
wolfMQTT-master/src/mqtt_broker.cRelevant lines:
2737: broker storesmc.keep_alive_sec.3577: broker responds toPINGREQwithPINGRESP.3607-3611: broker checks for 1.5 times Keep Alive timeout.The broker/server side therefore has logic for the
[MQTT-3.1.2-24]timeout rule. This does not remove the Client-side scheduling gap described in this document.Local Probe Result
A temporary local probe was compiled against the wolfMQTT source tree to check whether
MqttClient_WaitMessage()automatically sendsPINGREQ.Probe design:
readcallback always returnedMQTT_CODE_ERROR_TIMEOUT.writecallback counted writes and detected thePINGREQbytesC0 00.MqttClient_WaitMessage(&client, 1).MqttClient_Ping(&client)as a positive control.Observed output:
Interpretation:
MqttClient_WaitMessage()PINGREQMqttClient_Ping()PINGREQbytesC0 00The probe confirms that the core wait path does not automatically send Keep Alive pings. The temporary probe source and executable were removed after the test.
Why This Is an Inconsistency
The MQTT standard assigns the Keep Alive sending responsibility to the Client. For a complete client behavior, the implementation must ensure that the Client sends some MQTT Control Packet before the Keep Alive interval is exceeded. If no other Control Packet is sent, that packet must be
PINGREQ.wolfMQTT's core client library does not enforce this timing rule by itself:
PINGREQand wait forPINGRESP.MqttClient_WaitMessage()when the connection is idle.The mismatch is therefore:
PINGREQbefore the Keep Alive interval is exceededMqttClient_Ping()/MqttClient_Ping_ex()This is a real gap if the evaluated target is the core wolfMQTT client library itself. It is less severe than a malformed-packet acceptance bug because wolfMQTT provides the required API primitives and the examples show how an application can schedule pings correctly.
Reason:
The Keep Alive scheduling concerns describe the same underlying issue. wolfMQTT has Keep Alive support at the packet/API level, but the core client does not automatically maintain the Client-side Keep Alive obligation. A conforming application must schedule
MqttClient_Ping()/MqttClient_Ping_ex()before the negotiated Keep Alive interval expires.