You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug] MQTT QoS 1 and 2 are structurally unreachable because MqttActor never enables manualAcks, so mqtt.js PUBACKs while the message is still in the mailbox and MqttMessage.qos reports a guarantee the actor cannot provide #977
MqttQos documents 1 = at-least-once, 2 = exactly-once, MqttOptions.withQos(1|2) is offered and validated, and every inbound MqttMessage carries a qos field. None of that is achievable with the current wiring, because the actor never takes control of the acknowledgement.
mqtt.js acknowledges automatically: for QoS 1 it emits PUBACK, and for QoS 2 PUBREC, as soon as it has handed the packet to the 'message' listener — unless the client was constructed with manualAcks: true and the application calls client.handleMessage(packet, cb) itself. manualAcks does not appear anywhere in this repository, MqttConnectOptions has no field for it, and MqttClientLike declares no handleMessage. So there is no expressible way to defer the ack.
The framework's 'message' handler does exactly one thing: this.self.tell(...). That is a mailbox enqueue. The PUBACK is therefore on the wire while the message is still queued — and the actor mailbox is bounded drop-head by default, so it can be discarded after the broker has been told it arrived. A subscriber configured for QoS 2 gets the wire cost of exactly-once and the delivery guarantee of at-most-once.
The inbound path, src/io/broker/MqttActor.ts:455-466 — the listener only enqueues, and returns immediately, which is what lets mqtt.js ack:
src/io/broker/MqttActor.ts:455-466client.on('message',(topic,payload,packet)=>{// No user code on the mqtt.js loop: wrap into a lazily-decoding// payload and hand the message to our own mailbox.this.self.tell(mqttInboundSignal<T>({
topic,payload: newMqttPayload<T>(payload,this.codec(),topic),qos: (packet?.qos??0)asMqttQos,retain: packet?.retain??false,userProperties: packet?.properties?.userProperties,reasonCode: packet?.properties?.reasonCode,}));});
The connect options the actor builds, src/io/broker/MqttActor.ts:562-571 — no manualAcks:
grep -rn "manualAcks\|handleMessage\|puback" over src/, tests/, docs/ and the .conf files returns nothing.
Proposal
Two shapes, and the choice is a product decision rather than a technical one:
Make QoS real. Add manualAcks to MqttConnectOptions, handleMessage to MqttClientLike, keep the packet alongside the queued signal, and call handleMessage after onMessage returns (or after an explicit ack command, matching the AmqpActor / KafkaActor vocabulary the project already has — see [Feature] Unify the broker subscribe/unsubscribe command vocabulary across broker actors #670 on unifying it). This is the only version that makes withQos(2) mean what MqttQos says.
Or say so. Keep auto-ack, and change MqttQos's comment plus the MQTT docs page to state that the levels describe the broker-to-client transport, and that the framework acknowledges on receipt, so end-to-end the actor is at-most-once regardless of level.
Either way, the current combination — a validated withQos(2) and a doc promising exactly-once — should not survive.
Acceptance sketch
Either a QoS-1 subscription does not PUBACK until the actor has processed the message, or MqttQos and the MQTT docs page state that it acks on receipt.
manualAcks is reachable from MqttOptions if the first branch is taken.
A test with a fake MqttClientLike asserts the ordering of ack versus onMessage.
EN and DE docs updated together.
Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (v0.13.0) and re-verified before filing: confirmed by reading, plus a repository-wide grep. mqtt is not installed in this tree, so the PUBACK could not be observed on a wire; the finding does not depend on that — the option that would defer the ack has no representation in the code at all, which is a structural fact rather than a behavioural one.
Adjacent issues: #783 covers an external subscribe attaching arbitrary targets to # on the same actor; #743 covers TLS material. Neither touches QoS semantics.
Part of the production-readiness review batch — tracked in #913.
Problem
MqttQosdocuments1 = at-least-once, 2 = exactly-once,MqttOptions.withQos(1|2)is offered and validated, and every inboundMqttMessagecarries aqosfield. None of that is achievable with the current wiring, because the actor never takes control of the acknowledgement.mqtt.js acknowledges automatically: for QoS 1 it emits PUBACK, and for QoS 2 PUBREC, as soon as it has handed the packet to the
'message'listener — unless the client was constructed withmanualAcks: trueand the application callsclient.handleMessage(packet, cb)itself.manualAcksdoes not appear anywhere in this repository,MqttConnectOptionshas no field for it, andMqttClientLikedeclares nohandleMessage. So there is no expressible way to defer the ack.The framework's
'message'handler does exactly one thing:this.self.tell(...). That is a mailbox enqueue. The PUBACK is therefore on the wire while the message is still queued — and the actor mailbox is bounded drop-head by default, so it can be discarded after the broker has been told it arrived. A subscriber configured for QoS 2 gets the wire cost of exactly-once and the delivery guarantee of at-most-once.Evidence
The claim,
src/io/broker/MqttMessages.ts:10-11:The inbound path,
src/io/broker/MqttActor.ts:455-466— the listener only enqueues, and returns immediately, which is what lets mqtt.js ack:The connect options the actor builds,
src/io/broker/MqttActor.ts:562-571— nomanualAcks:The client surface,
src/io/broker/MqttActor.ts:599-610— nohandleMessage, so even a caller who wanted to defer could not:grep -rn "manualAcks\|handleMessage\|puback"oversrc/,tests/,docs/and the.conffiles returns nothing.Proposal
Two shapes, and the choice is a product decision rather than a technical one:
manualAckstoMqttConnectOptions,handleMessagetoMqttClientLike, keep the packet alongside the queued signal, and callhandleMessageafteronMessagereturns (or after an explicit ack command, matching theAmqpActor/KafkaActorvocabulary the project already has — see [Feature] Unify the broker subscribe/unsubscribe command vocabulary across broker actors #670 on unifying it). This is the only version that makeswithQos(2)mean whatMqttQossays.MqttQos's comment plus the MQTT docs page to state that the levels describe the broker-to-client transport, and that the framework acknowledges on receipt, so end-to-end the actor is at-most-once regardless of level.Either way, the current combination — a validated
withQos(2)and a doc promising exactly-once — should not survive.Acceptance sketch
MqttQosand the MQTT docs page state that it acks on receipt.manualAcksis reachable fromMqttOptionsif the first branch is taken.MqttClientLikeasserts the ordering of ack versusonMessage.Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (
v0.13.0) and re-verified before filing: confirmed by reading, plus a repository-wide grep.mqttis not installed in this tree, so the PUBACK could not be observed on a wire; the finding does not depend on that — the option that would defer the ack has no representation in the code at all, which is a structural fact rather than a behavioural one.Adjacent issues: #783 covers an external
subscribeattaching arbitrary targets to#on the same actor; #743 covers TLS material. Neither touches QoS semantics.Part of the production-readiness review batch — tracked in #913.