Zephyr OS MQTT Keepalive Loops: Schedule Maintenance Even When No Socket Data Arrives
A quiet MQTT connection still needs an application loop that services its keepalive deadline. If a Zephyr OS client waits indefinitely for incoming socket data and calls maintenance only afterward, silence can prevent the very ping intended to keep the connection usable. Treat socket readiness and protocol maintenance as separate reasons for the loop to wake.
This article follows Zephyr v4.4.0 MQTT client behavior. It proposes event-loop checks, not a measured broker timeout or a universal reconnect interval. Network delay, broker configuration and the application's other service deadlines must be assessed for the actual deployment.

Derive the wait from the maintenance deadline
The tagged MQTT API header requires periodic mqtt_live calls and offers mqtt_keepalive_time_left as a source for a poll timeout. The latter reports milliseconds until the next expected keepalive or minus one when keepalive is disabled. It is a scheduling input, not an indication that new payload data has arrived.
Before waiting, combine that deadline with any other deadline the application owns. After waking, reevaluate maintenance even when the socket has no readable data. A timeout wake is useful work in this design. Do not place the only mqtt_live call inside a branch that executes exclusively on incoming packet readiness.
Keep the signed disabled sentinel distinct from an unsigned duration. Converting minus one into a large positive timeout may appear to work in one wrapper while breaking the intended meaning elsewhere. If keepalive is disabled, other application deadlines and shutdown responsiveness still determine how long the loop may wait.
Interpret not-due separately from a failed ping
The v4.4.0 implementation returns minus EAGAIN from mqtt_live when it did not attempt a ping. If the elapsed activity interval reaches an enabled keepalive duration, it attempts mqtt_ping and returns that result. The time-left helper returns zero once the deadline is due.
An application that disconnects on every negative mqtt_live result can therefore tear down a healthy connection during an early maintenance call. Handle the documented implementation's not-due outcome deliberately, while preserving other failures for diagnosis. Do not broadly ignore all negative results just to suppress these reconnects.
Equally, a successful ping-send path is not proof that the broker responded. Receiving and processing protocol traffic remains part of the loop. A product should have an explicit policy for identifying an unresponsive connection rather than treating one maintenance return as a complete end-to-end health check.
Bound unrelated work between wakeups
Consider a hypothetical gateway that performs a slow local storage operation after every received command. Its poll timeout can be correct while its next maintenance call still arrives too late because the callback or subsequent work blocks the loop. Include processing time when reviewing deadline service, not just the duration passed to poll.
Move lengthy application work into a separately controlled path when the client design permits it. Preserve the client's synchronization and payload-lifetime rules when doing so. Merely adding a high-priority maintenance thread can create concurrent client access or ownership problems if the rest of the application was designed around one event-loop owner.
Recalculate the next wait after activity and after processing; a timeout value copied once at connection startup is not a continuing deadline calculation. Record the elapsed interval around the whole iteration when investigating missed maintenance.
Exercise silence as a normal case
A proposed test set includes a connected broker with no application traffic, frequent inbound traffic, a slow application handler, an already-due maintenance call and keepalive disabled. Verify that the quiet case still reaches maintenance and that an early call does not trigger a reconnect solely because it returns minus EAGAIN.
Then test actual transport failure separately, preserving the failing API result and connection state. The expected outcome is a loop that services time-based protocol work without waiting for unrelated input, while keeping not-due, attempted-send and confirmed-response observations distinct.