All notable changes to this project will be documented in this file.
[0.10.0] - 2026-05-07
Spec-compliance and correctness sweep against MQTT 5.0 (OASIS), plus server/ client robustness fixes. EMQX interop suite (49 tests against a live broker) remains 100% green; no public API changes.
Changed (potentially breaking at the wire level — stricter spec compliance)
- Default
protocol_versionis now5(was4). Library is marketed "MQTT 5.0" and v5 features (topic aliases, AUTH, properties, reason codes) were silently dropped under the v3.1.1 default.MqttX.Client.connect(protocol_version: 4)to opt in to v3.1.1. - Server now rejects unsupported protocol versions with CONNACK
0x84(v5) /0x01(v3.x). Default allowlist[3, 4, 5]configurable via:supported_versionsintransport_opts.
Fixed (codec — MqttX.Packet.Codec / MqttX.Packet.Properties)
- PUBLISH with QoS=3 rejected as Malformed Packet (§3.3.1.2)
- PUBLISH with DUP=1 + QoS=0 rejected (§3.3.1.1)
- CONNECT reserved bit must be 0 (§3.1.2.3); non-zero fixed-header flags rejected (§3.1.1)
- Will Flag=0 with non-zero Will QoS or Will Retain rejected (§3.1.2.6/7)
- Will QoS=3 rejected (§3.1.2.6)
- v3.x username flag=0 with password flag=1 rejected (§3.1.2.9)
- Empty SUBSCRIBE / UNSUBSCRIBE payload rejected as Protocol Error (§3.8.3 / §3.10.3)
- Subscription Options reserved bits non-zero, QoS=3, RH=3 rejected (§3.8.3.1)
- DISCONNECT and AUTH 1-byte forms (reason code only, no property length) accepted per §3.14.2.2.1 / §3.15.2.2.1
- UTF-8 strings now reject U+0000 (null) and U+D800–U+DFFF (surrogates) per §1.5.4
- Malformed CONNECT no longer crashes the codec with
MatchError; surfaces:malformed_packet - Invalid SUBACK/UNSUBACK reason bytes return
:malformed_packetinstead of crashing - Properties — duplicate non-User-Property properties rejected as Protocol Error (§2.2.2.2)
- Property value-0 rejection: Subscription Identifier (§3.8.2.1.2), Receive Maximum (§3.1.2.11.3), Maximum Packet Size (§3.1.2.11.4)
- Boolean properties (Payload Format Indicator, Request Problem Information, Retain Available, etc.) reject non-0/1 byte values
- Maximum QoS rejects values > 2
- User-Property accumulation switched from O(n²)
++ [val]to prepend + reverse-once - Subscription-Identifier list switched from O(n²)
++ [val]to prepend + reverse-once
Fixed (topic — MqttX.Topic)
+/#filters at the first level no longer match$SYS/...-style topics (§4.7.2)- Topic length capped at 65535 bytes (§1.5.4)
- Shared subscription
$share/<group>/...rejects+or#in ShareName (§4.8.2) flatten/1switched from O(n²) binary concat to iolist +Enum.intersperse
Fixed (server — MqttX.Transport.Handler / MqttX.Server.Router / MqttX.Server.RateLimiter)
Outgoing QoS 1 retransmission on the server (§4.4). Outbound QoS 1 PUBLISHes are now tracked in
pending_qos1_tx; the existing periodic retry timer re-sends withDUP=trueafterqos2_retry_intervalms, dropping afterqos2_max_retriesattempts. PUBACK arrival clears the entry. Previously QoS 1 outbound was fire-and-forget — a lost PUBACK silently dropped the message.Receive Maximum applies to QoS 1 inbound as well as QoS 2 (§3.1.2.11.3). Previously the limit was only checked for QoS 2; a client could fill the flow-control window with un-PUBCOMP'd QoS 2 messages and then push QoS 1 publishes through unbounded. The handler now responds to a QoS 1 PUBLISH exceeding the limit with
PUBACKreason0x93(Receive Maximum exceeded). QoS 2 excess continues to receivePUBREC 0x93(spec permits either per-message ack or DISCONNECT).Critical: retained-publish packet IDs now come from
next_packet_idinstead of:rand.uniform/1— was colliding with the QoS sequence allocator and breaking ack matchingWill message published exactly once on keepalive timeout /
handle_close/handle_error(was double-publishing through two paths)DISCONNECT reason code 0x04 publishes the Will; other reason codes suppress it (§3.14.4) — previously the reason code was ignored
Empty topic with un-mapped Topic Alias triggers DISCONNECT 0x94 (§3.3.4) — previously dispatched an empty-topic PUBLISH to the user handler
Outbound oversize-drop rolls back the packet_id allocation (was leaking)
Duplicate PUBREC after PUBREL no longer re-sends PUBREL (§4.3.3 fig 4.4)
AUTH-before-CONNECT now sends DISCONNECT 0x82 instead of leaving the handler in a broken state (CONNACK with
protocol_version: nil)Will Delay Interval timers (§3.1.3.2.2) now owned by a supervised
MqttX.Server.WillDelayGenServer under the application supervisor; cancelled on same-client_idreconnectRate-limiter ETS table now created with
read_concurrency: true
Fixed (client — MqttX.Client.Connection)
- Critical: retry-loop reducer arity bug fixed — was crashing with
MatchErroronce both{:rx, _}and{:tx, _}pending_acksentries coexisted on the same connection keepalive == 0correctly disables the keepalive timer (was scheduling a zero-millisecond tight loop)- PINGRESP timeout: client now arms a deadline at
keepalive*1500mson every PINGREQ; if PINGRESP doesn't arrive, the socket is torn down and reconnect is scheduled. Half-dead brokers no longer require TCP teardown to detect. session_presentfrom CONNACK now surfaced to the handler in the:connectedevent; warns on MQTT-3.2.2-2 violation (clean_session=truebutsession_present=true)- Server-initiated DISCONNECT now closes the socket immediately and schedules
reconnect (was waiting for
:tcp_closedto land separately) - AUTH continuation in
wait_for_connackre-armsset_socket_active/1so multi-step AUTH doesn't hang after the first packet - AUTH packet property names corrected:
:auth_method/:auth_data→:authentication_method/:authentication_data next_packet_idskips IDs currently inpending_acks(§2.2.1)schedule_reconnectcancels any existing reconnect timer (no more stacked timers when several disconnect events fire)set_socket_activeguards againstnilsocket (server-disconnect race)- Pending SUBACK/UNSUBACK callers monitored via
Process.monitor; entry dropped on:DOWN(was leaking afterGenServer.calltimeouts)
Fixed (WebSocket client — MqttX.Client.WebSocket)
Sec-WebSocket-Acceptnow validated asBase64(SHA1(client_key + magic GUID))per RFC 6455 §4.1; status line strictly matched asHTTP/1.x 101 …Sec-WebSocket-Protocolecho checked (warn-only on missing — common with MQTT brokers that nonetheless speak it correctly)- FIN-bit fragmentation: opaque
frag_statethreaded acrossdecode_frames/2calls so multi-frame messages split across TCP reads reassemble correctly - Frame masking switched from byte-by-byte recursion to
:crypto.exor/2against a pre-padded mask buffer (orders-of-magnitude faster on large payloads)
Added
MqttX.Session.ETSOwner— a long-lived owner of the default:mqttx_sessionsETS table under the application supervisor. Previously the table was owned by whichever client first calledMqttX.Session.ETSStore.init/1, so all sessions were lost when that process exited. The table is now created withread_concurrency: true, write_concurrency: true.MqttX.Server.WillDelay— supervised GenServer that owns Will Delay Interval timers across connection lifecycles, with per-client_idcancellation.:supported_versionsserver transport option (default[3, 4, 5]).
Documentation
AGENTS.md— usage guide for AI coding assistants integrating MqttX into projects: mental model (client vs broker), transport selection, idiomatic patterns (receive-on-client, broker↔PubSub bridge, persistent sessions, custom auth), and a curated list of mistakes commonly made (publishing wildcards, confusinghandle_publishwithhandle_mqtt_event, randomclient_idper connect, default-to-QoS-2,$SYSexclusion). Shipped in the hex package and rendered on hexdocs.CONTRIBUTING.md— repo orientation for contributors: layout, test commands, known test-environment couplings, and the deferred TODO carried over from the v0.9.0 spec sweep.CLAUDE.mdis now a symlink toAGENTS.mdfor tool compatibility.- README "Common Patterns" section with worked examples for receiving
messages on the client (
handle_mqtt_event/3), broadcasting from the server viahandle_info/2, and resuming MQTT 5.0 sessions withsession_expiry_interval. - README "Common Pitfalls" section covering session-store /
clean_sessioninteraction, server keepalive override, max-packet-size enforcement, publish-vs-subscribe wildcard rules, and$SYStopic exclusion.
[0.9.0] - 2026-03-30
Added
- Server keepalive override (MQTT 5.0): Configurable
server_keep_aliveintransport_opts— server sends keepalive override in CONNACK and uses it for the keepalive timer when protocol version >= 5 handle_connect/4callback (optional): Newhandle_connect(client_id, credentials, connect_info, state)callback that receives connection metadata (protocol_version,keep_alive) separately from credentials. Existinghandle_connect/3continues to work unchanged —handle_connect/4takes precedence when defined
[0.8.0] - 2026-03-11
Added
- Complete MQTT 5.0 Client Compliance: Closed all remaining client-side spec gaps
- server_keep_alive: Client applies server's keepalive override from CONNACK (§3.2.2.3.14)
- assigned_client_identifier: Client adopts server-assigned client ID from CONNACK (§3.2.2.3.7)
- maximum_packet_size: Client enforces server's maximum packet size from CONNACK; oversized outgoing packets return
{:error, :packet_too_large}(§3.2.2.3.6) - server_reference: Client parses and logs server redirect on CONNACK rejection and server DISCONNECT (§3.2.2.3.18)
- Enhanced AUTH: Client handles multi-step AUTH exchange during and after CONNECT via
handle_auth/3callback (§4.12)
- EMQX Cloud Interop: 49 automated interop tests against EMQX Cloud broker
- SUBACK Reason Code Checking:
subscribe/3now returns{:ok, [granted_qos]}or{:error, {:subscription_refused, acks}}based on actual SUBACK response - Outgoing Topic Aliases (MQTT 5.0): Client automatically assigns and reuses topic aliases for repeated publish topics, reducing bandwidth
- DISCONNECT with Reason Code:
disconnect/2accepts:reason_codeand:propertiesoptions for MQTT 5.0 graceful disconnect - WebSocket Client Transport: Connect to brokers over WebSocket with
transport: :wsortransport: :wss(RFC 6455 binary framing) - Reason String Surfacing: Server reason strings from SUBACK, UNSUBACK, PUBACK, DISCONNECT are logged automatically
Fixed
- Formatting issues across multiple files (CI compliance)
- Dialyzer
callback_type_mismatchin WebSocket transportclose/1(now returns:okper behaviour spec) - WebSocket frame decoder byte-alignment bug in
decode_one_frame(mask_bit extraction)
[0.7.0] - 2026-03-05
Added
- Full MQTT 3.1/3.1.1/5.0 Compliance: Closed all remaining spec compliance gaps
- Pre-CONNECT packet rejection: Non-CONNECT/AUTH packets before CONNECT now trigger DISCONNECT 0x82 (Protocol Error) per spec
- Topic alias validation: Incoming topic aliases are validated against
topic_alias_maximum; out-of-range aliases trigger DISCONNECT 0x94 - MQTT 5.0 property forwarding: Outgoing PUBLISH packets now forward properties (user_properties, content_type, correlation_data, etc.) from handler callbacks
- CONNACK capability properties: Server advertises
retain_available,wildcard_subscription_available, andsubscription_identifier_availablein CONNACK for MQTT 5.0 connections - retain_handling support: Subscription option
retain_handling: 2suppresses retained message delivery on subscribe - no_local support:
Router.match/3andRouter.match_and_advance/3accept optionalpublisherparameter to filter out subscriptions withno_local: true - Client server DISCONNECT handling: Client now handles server-initiated DISCONNECT packets, notifying the handler with
{:server_disconnect, reason_code}
- QoS 2 Retransmission & DUP Handling (Server): Periodic retry timer re-sends PUBREC/PUBLISH(dup)/PUBREL for stale in-flight QoS 2 messages; drops after configurable max retries. DUP incoming PUBLISH re-sends PUBREC without re-delivering.
- Topic Aliases (MQTT 5.0 Server): Incoming PUBLISH with
topic_aliasproperty resolved automatically. Server advertisestopic_alias_maximumin CONNACK. Alias-only publishes (empty topic) look up stored mapping. - Flow Control / Receive Maximum (MQTT 5.0 Server): Server enforces
receive_maximumfor incoming QoS 2 messages. Excess publishes receive PUBREC with reason code0x93(Receive Maximum exceeded). Server advertisesreceive_maximumin CONNACK. - Maximum Packet Size (MQTT 5.0 Server): Configurable
max_packet_sizeoption. Oversized incoming packets trigger DISCONNECT with reason code0x95(Packet too large). Outgoing publishes exceeding client'smaximum_packet_sizeare silently dropped. Server advertisesmaximum_packet_sizein CONNACK when configured. - WebSocket Transport: MQTT over WebSocket via Bandit, supporting all MQTT protocol features over
ws://andwss://connections. - Mosquitto Validation Suite: 104 automated tests against Mosquitto clients across TCP and WebSocket transports, covering all protocol versions and MQTT 5.0 features.
- Handler tests: 30+ new tests covering compliance features, QoS 2 full flow, DUP handling, retry timer, CONNACK properties, topic aliases, flow control, max packet size, and server-initiated DISCONNECT.
Changed
- Codec: MQTT 5.0 PUBLISH with empty topic is now valid when
topic_aliasproperty is present (per MQTT 5.0 spec section 3.3.2.1) - QoS 2 pending entries:
pending_qos2_rxentries now include timestamps and retry counts;pending_qos2_txentries are enriched maps with phase, packet, timestamp, and retry info - Router API:
match/2→match/3andmatch_and_advance/2→match_and_advance/3with optionalpublisherparameter (backward compatible, defaults tonil)
Fixed
- Server PUBREL handler now correctly extracts packet/opts from both legacy 2-tuple and new 4-tuple
pending_qos2_rxentries
[0.6.1] - 2026-03-02
Fixed
- Logo rendering on hexdocs.pm (use absolute URL for README image)
- CI: increased GenServer.stop timeout in client tests for slower runners
- CI: skip JSON payload tests on OTP < 27
- Removed accidentally committed mqttx-0.1.0 directory
[0.6.0] - 2026-03-02
Added
- Connection Supervision:
MqttX.Client.SupervisorDynamicSupervisor for managed client connectionsMqttX.Client.connect_supervised/1starts connections under the supervisor with automatic crash recoveryMqttX.Client.list/0lists all registered connections viaMqttX.ClientRegistryMqttX.Client.whereis/1looks up connections by client_id- Connections auto-register in
MqttX.ClientRegistryon init
- Rate Limiting: Per-client connection and message rate limiting for MQTT servers
MqttX.Server.RateLimitermodule with ETS-based atomic counters- Connection rate limiting (configurable max connections per interval)
- Per-client message rate limiting (configurable max messages per client per interval)
- MQTT 5.0 reason code
0x96(message_rate_too_high) sent for rate-limited QoS 1+ publishes - Integrated into both ThousandIsland and Ranch transport adapters
- Configured via
:rate_limitoption inMqttX.Server.start_link/3
- Capacity Planning guide: Device-per-vCPU sizing tables for IoT workloads (sleepy sensors through real-time streaming), instance sizing recommendations
- Performance & Scaling guide: Architecture decisions, trie router internals, VM/OS tuning, and deployment guidelines
- Project Branding: MqttX logo in README and hexdocs
- EMQX interop test suite: 49 tests against live EMQX broker covering MQTT 5.0 features
- Server Keepalive Timeout: Disconnects clients that stop sending packets within 1.5x
keep_aliveseconds (MQTT spec compliance)- Automatic timer start after CONNACK, reset on every received packet
- Will message published on keepalive timeout (ungraceful disconnect)
- Will Delay Interval (MQTT 5.0): Delays will message publication by
will_delay_intervalseconds after ungraceful disconnectwill_delay_interval: 0(or MQTT 3.1.1) publishes immediately (backward compatible)- Will properties forwarded to handler
- Session Expiry Timer (MQTT 5.0): Fires
handle_session_expired/2callback aftersession_expiry_intervalseconds post-disconnect0= expire immediately,0xFFFFFFFF= never expire- New optional
handle_session_expired/2callback inMqttX.Serverbehaviour
- Server-Initiated Disconnect: Kick clients with MQTT 5.0 reason codes
MqttX.Server.disconnect/3sends DISCONNECT and closes connection{:disconnect, reason_code, state}return type fromhandle_publish,handle_subscribe,handle_unsubscribe,handle_info- Ranch transport now properly forwards
handle_infomessages to handler (was silently dropping them)
Changed
- Trie-based Topic Router: Replaced O(N) linear scan with a trie data structure for O(L+K) topic matching — independent of total subscription count. Same public API.
- iodata Encoding: Socket sends use
Codec.encode_iodata/2in all transports, avoiding binary copies on every packet - Empty-buffer fast path: Skips binary concatenation when the TCP buffer is empty (common case)
- Cached callback dispatch:
function_exported?computed once at connection init, not per message - Direct inflight counter: O(1) flow control check instead of scanning pending_acks
- Retained message delivery: Exact topic subscriptions use O(1) ETS lookup instead of full table scan
Fixed
- Handler state lost on callbacks:
notify_handlernow correctly returns updated handler state (was silently discarding it) - Missing retries field in pending_acks: QoS 1/2 pending_acks entries now include
retries: 0(prevented retry tracking) - Session not saved on socket close: Session data now persists on unexpected TCP close/error, not just clean disconnect
- Queued messages not delivered on reconnect: Buffer is now processed after CONNACK for persistent sessions
- Protobuf codec crash on non-protobuf structs: Now returns
{:error, {:protobuf_encode_error, _}}instead of raising - Protobuf codec crash on unknown module: Now returns
{:error, {:unknown_message_module, module}}instead of raising - Removed dead outgoing topic alias code (
topic_to_alias,next_alias) that was never functional MqttX.version/0now returns correct version string- Guides now included in hex.pm docs
[0.5.0] - 2026-01-15
Added
- Telemetry Integration: Comprehensive
:telemetryevents for observability- Client events: connect, disconnect, publish, subscribe, message
- Server events: client_connect, client_disconnect, publish, subscribe
- New
MqttX.Telemetrymodule with helper functions
- Shared Subscriptions (MQTT 5.0):
$share/group/topicpattern for load balancing- Round-robin distribution across group members
Router.match_and_advance/2for stateful distribution- Automatic group cleanup when last member leaves
- Topic Alias (MQTT 5.0): Bandwidth reduction for repeated topics
- Client stores
topic_alias_maximumfrom CONNACK - Resolves incoming topic aliases automatically
alias_to_topicmap in connection state
- Client stores
- Message Expiry (MQTT 5.0): Respects
message_expiry_intervalproperty- Retained messages stored with timestamp
- Expired messages skipped on delivery
- Remaining expiry sent in delivered messages
- Flow Control (MQTT 5.0): Enforces
receive_maximumfor backpressure- Client tracks inflight QoS 1/2 message count
- Returns
{:error, :flow_control}when limit reached - Stores
receive_maximumfrom CONNACK
- Enhanced Auth (MQTT 5.0): SASL-style authentication callback
- New
handle_auth/3callback inMqttX.Serverbehaviour - Default implementation returns error (not supported)
- New
- Request/Response (MQTT 5.0): Helper for request/response pattern
MqttX.Client.request/4function- Passes
response_topicandcorrelation_dataproperties :propertiesoption inpublish/4
Changed
- Transport adapters store retained messages with expiry metadata (5-tuple ETS format)
- Client connection state includes topic alias and receive_maximum fields
[0.4.0] - 2026-01-15
Added
- TLS/SSL Client Support: Optional TLS via
:transportoption (:tcpor:ssl):ssl_optsfor SSL configuration (verify, cacerts, etc.)- Default port 8883 for SSL connections
- QoS 2 Complete Flow: Full PUBREC/PUBREL/PUBCOMP handshake implementation
- Client tracks outgoing QoS 2 messages through all phases
- Client handles incoming QoS 2 messages correctly
- Message Inflight Tracking: Timer-based retry for unacknowledged QoS 1/2 messages
- Configurable
:retry_intervaloption (default: 5000ms) - Automatic retry with
dup: trueflag - Max 3 retries before dropping message
- Configurable
- Retained Messages: Server stores and delivers retained messages
- ETS-based storage per server instance
- Delivered to new subscribers on SUBSCRIBE
- Empty payload clears retained message
- Will Message Delivery: Server publishes will message on ungraceful disconnect
- Stored from CONNECT packet
- Published when connection closes without DISCONNECT
- Supports retained will messages
- Session Persistence: Configurable session storage for
clean_session: falseMqttX.Session.Storebehaviour for custom implementationsMqttX.Session.ETSStorebuilt-in in-memory store- Saves/restores subscriptions, pending messages, packet IDs
Changed
- Client connection state now tracks subscriptions for session persistence
- Transport adapters create ETS tables for retained messages
[0.3.0] - 2026-01-15
Added
- MQTT vs WebSocket JSON performance comparison in README
- Comprehensive API reference documentation in README
- New test files for improved coverage:
backoff_test.exs- exponential backoff logic testsproperties_test.exs- MQTT 5.0 properties encode/decode testsclient_test.exs- client API testsserver_test.exs- server behaviour and callback tests
- MQTT 5.0 packet tests (AUTH, DISCONNECT with reason codes, properties)
- MQTT 3.1 packet tests
- Edge case tests (empty payload, large payload, max packet ID, unicode topics)
Changed
- Updated ThousandIsland dependency to
~> 1.4(was~> 1.0) - Updated Ranch dependency to
~> 2.2(was~> 2.1) - Updated Protox dependency to
~> 2.0(was>= 1.7.0)
Fixed
- Formatting issues in
thousand_island.ex - Protobuf payload codec updated for Protox 2.0 API changes (encode returns 3-tuple)
[0.2.0] - 2026-01-15
Added
handle_info/2callback for MqttX.Server to handle custom messages (e.g., PubSub)- Support for outgoing PUBLISH via
{:publish, topic, payload, state}return value - Enables bidirectional communication (server can push messages to connected clients)
[0.1.6] - 2026-01-15
Changed
- Broadened protox dependency to support both 1.x and 2.x (
>= 1.7.0)
[0.1.5] - 2026-01-15
Added
- GitHub Actions CI workflow (tests on Elixir 1.17-1.19, OTP 27-28, dialyzer)
- Roadmap section in README
- Username/password example in client documentation
- Changelog link on hex.pm package page
- Hex.pm, Docs, and CI badges to README
Changed
- Documentation landing page now shows README instead of module docs
Fixed
- JSON payload codec now conditionally compiles only on OTP 27+
- Code formatting issues
- Version test no longer hardcodes version string
- Dialyzer false positives for defensive pattern matching
[0.1.1] - 2026-01-15
Added
- GitHub Actions CI workflow (tests, formatting, dialyzer)
- Roadmap section in README
- Username/password example in client documentation
- Changelog link on hex.pm package page
Fixed
- JSON codec description now correctly references built-in Erlang/OTP 27+ module
[0.1.0] - 2026-01-14
Added
- Initial release
- MQTT packet codec supporting MQTT 3.1, 3.1.1, and 5.0
- All 15 MQTT packet types
- MQTT 5.0 properties support
- ThousandIsland transport adapter
- Ranch transport adapter
- MQTT Server behaviour with handler callbacks
- Topic router with wildcard support (+, #)
- MQTT Client with automatic reconnection
- JSON payload codec (via built-in Erlang/OTP 27+ JSON module)
- Protobuf payload codec (via Protox)
- Raw binary payload codec
- Comprehensive test suite