Inbound and outbound packet-size checks differ, so a QoS 1 message just under MaxUserPayloadBytes is accepted but never delivered (blocks a persistent subscriber)
@abhiramaab is already working on this.
Since Oct 2, 2026.
- #314 by @abhiramaab — closed without merging
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- java
- Domain
- backend, networking
Research direction
Start with ChannelAttrs.setMaxPayload and the inbound MqttDecoder(maxUserPayloadSize), then compare that limit with MQTT3MessageSizer and MQTTPacketFilter on outbound delivery. Inspect MQTTSessionHandler.writeConfirmableSubMessage and the related resend behavior; done means accepted messages are deliverable or are dropped and confirmed without blocking the subscriber's in-flight window.
Written by the indexing model from the issue text.
Description
Version: 4.0.0-incubating (standalone, default DevOnlySettingProvider)
Summary
MaxUserPayloadBytes (stored as TenantSettings.maxPacketSize) is enforced differently in the two directions:
- Inbound (publisher → broker): the replacement
MqttDecoder(maxPacketSize)compares the fixed header's remaining length R against the limit. A packet is accepted while R ≤ L. - Outbound (broker → subscriber):
MQTTPacketFilterdrops a packet whose full encoded size1 + varint(R) + Ris over the limit (MQTT3MessageSizer), reportingOversizePacketDroppedand never completing the write.
So a QoS 1 PUBLISH whose remaining length is within 3 bytes of the limit (R = L−2 … L for L = 16384) is accepted and PUBACKed from the publisher, stored in the subscriber's inbox, and then dropped on every delivery attempt. writeConfirmableSubMessage keeps it in the unconfirmed set and resend() retries it until MaxResendTimes. With a large MaxResendTimes, a persistent subscriber's in-flight window fills with undeliverable messages and no further QoS 1 is delivered to it until the session is reset.
For MQTT 5 subscribers this cannot be worked around from the client: MQTT5ConnectHandler rejects a client Maximum Packet Size above maxPacketSize (CONNACK 0x83), and MQTTPacketFilter uses min(clientMax, settings.maxPacketSize).
Reproduction
- Start the broker with
-DMaxUserPayloadBytes=16384(and a large-DMaxResendTimes). - Subscriber: MQTT 3.1.1,
cleanSession=false, QoS 1, manual acks. - Publisher (same tenant): QoS 1 publishes with remaining length 16382, 16383, 16384 (payload = R − 2 − topicLength − 2), followed by a small normal message.
- The publisher gets PUBACKs for all of them; the subscriber receives none (not even the normal message once ~8 undeliverable messages occupy the window).
Expected
Both directions use the same measure, e.g. the inbound decoder limit is maxPacketSize − 5 (the maximum fixed-header overhead), or the outbound filter compares the remaining length, so anything accepted inbound is deliverable to a subscriber with the same limit. Independently, MQTTSessionHandler.writeConfirmableSubMessage already drops-and-confirms a confirmable message for NoSubPermission, Duplicated and NoLocal; doing the same when the encoded size exceeds the subscriber's limit (report a drop event, confirm(packetId, false)) would stop an undeliverable message from occupying the in-flight window until MaxResendTimes.
Checked against main (2026-10-02, 1394131): still present. #279 ("Fix oversized packet mem leakage", merged 2026-08-31, not yet released) makes MQTTPacketFilter release the buffer and complete the write promise, but the message stays unconfirmed in the session, and the inbound MqttDecoder(maxUserPayloadSize) in ChannelAttrs.setMaxPayload still compares the remaining length.
Related: apache/bifromq#258 (resend cancelled by PUBACK), apache/bifromq#289 (resend chain stops after a MaxRetried drop), #279 (oversized-packet buffer leak).
- Dominant language
- Java
- Stars
- 805
- Forks
- 99
- Avg merge
- 12h 35m
- Merged PRs (30d)
- 3
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from apache/bifromq
-
[BUG] KVRangeRestorer leaves the restore future uncompleted when startRestore() throws, permanently wedging the rangePossibly taken @ImDanXie claimed this 34 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
[BUG] TopicUtil.isValidTopicFilter accepts "$share/g/" — a shared subscription with an empty filter that can never matchPossibly taken @efegokdemir claimed this 35 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
[BUG] UTF8Util.isWellFormed inverts the surrogate-pair check: valid non-BMP characters are rejected, lone surrogates acceptedPossibly taken @efegokdemir claimed this 35 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
[BUG] ManagedRequestPipeline.close() leaves in-flight request futures pending foreverPossibly taken @ImDanXie claimed this 34 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 68/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
snowflakedb/snowflake-kafka-connector#1610 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
swagger-api/swagger-petstore#245 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
liquid-java/liquidjava#443 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
quarkiverse/quarkus-google-cloud-services#1188 ·
Maintainers usually reply within 1 day