| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| EMQX Enterprise (LTS) 6.3.3 source code.tar.gz | < 17 hours ago | 14.4 MB | |
| EMQX Enterprise (LTS) 6.3.3 source code.zip | < 17 hours ago | 16.9 MB | |
| README.md | < 17 hours ago | 97.6 kB | |
| Totals: 3 Items | 31.4 MB | 0 | |
Download
Ubuntu / Debian
| OS | Arch | Package | Tarball |
|---|---|---|---|
ubuntu24.04 |
amd64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
ubuntu24.04 |
arm64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
ubuntu22.04 |
amd64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
ubuntu22.04 |
arm64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian13 |
amd64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian13 |
arm64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian12 |
amd64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian12 |
arm64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian11 |
amd64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
debian11 |
arm64 | .deb (sha256) | .tar.gz (www.emqx.com) (sha256) |
RHEL / Rocky / Amazon Linux
| OS | Arch | Package | Tarball |
|---|---|---|---|
el10 |
amd64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el10 |
arm64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el9 |
amd64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el9 |
arm64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el8 |
amd64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el8 |
arm64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
amzn2023 |
amd64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
amzn2023 |
arm64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
el7 |
amd64 | .rpm (sha256) | .tar.gz (www.emqx.com) (sha256) |
macOS
| OS | Arch | Package |
|---|---|---|
macos15 |
arm64 | .zip (sha256) |
macos26 |
arm64 | .zip (sha256) |
Plugins
| Plugin | Version | Package |
|---|---|---|
emqx_acme |
0.2.0 | .tar.gz (www.emqx.com) (sha256) |
emqx_agent |
1.0.0 | .tar.gz (www.emqx.com) (sha256) |
emqx_backup_sync |
0.1.4 | .tar.gz (www.emqx.com) (sha256) |
emqx_bcast |
0.4.4 | .tar.gz (www.emqx.com) (sha256) |
emqx_bridge_mqtt_dq |
0.5.2 | .tar.gz (www.emqx.com) (sha256) |
emqx_maptabs |
0.1.2 | .tar.gz (www.emqx.com) (sha256) |
emqx_offline_messages |
2.0.2 | .tar.gz (www.emqx.com) (sha256) |
emqx_relup |
1.0.3 | .tar.gz (www.emqx.com) (sha256) |
emqx_sync_request |
0.1.2 | .tar.gz (www.emqx.com) (sha256) |
emqx_unsgov |
0.1.4 | .tar.gz (www.emqx.com) (sha256) |
emqx_username_quota |
1.2.3 | .tar.gz (www.emqx.com) (sha256) |
Summary
6.3.3 focuses on lower CPU and memory usage, connection resilience and operational reliability, with 42 enhancements and 130 bug fixes.
- Performance: Reduced overhead for MQTT connections, message processing, topic matching and ACL checks.
- TLS: Handshake overload protection and a shared outbound trust store (
node.trusted_certs). - Reliability: Fixes for durable sessions, queues and streams, shared subscriptions, connector retries and configuration merges.
- Deployment: Pinned plugins, optional RPC compression, IPv6 for Kafka-family connectors and IAM roles for Kinesis.
- Upgrade notes: Review changed defaults for queue/stream creation and backend failures, stricter HTTP/WebSocket limits, and corrected connector timing.
See the full list below.
Upgrade Notes
The following changes can alter the behavior of an existing deployment.
mq.auto_createandstreams.auto_createnow default to disabled, so a subscription to a$queue/...or$stream/...topic no longer creates a queue or a stream (#18788).authentication_settings.ignore_backend_failuresandauthorization.ignore_backend_failuresnow default toper_security_profile, and under thelegacyprofile a storedfalsedenies the client when a backend fails (#19088).- MQTT v5 CONNECT and Will properties now accept at most 100 User Property pairs, set by the new
mqtt.max_connect_user_properties(#18974). - Dashboard OIDC and SAML SSO logins must now complete in the browser that started them. SAML also requires an HTTPS address in
dashboard_addrunlessskip_login_cookie_check = trueis set for the backend (#18810). - At startup, EMQX removes
gateways,publish,auditandmfa_managementfrom the stored scope lists of existing namespaced Dashboard viewers. Existing API keys are unchanged (#19125). - MQTT and gateway WebSocket listeners now enforce
websocket.max_frame_sizeon complete messages, including after decompression. On MQTT listeners, the defaultinfinitynow limits a message to twicemqtt.max_packet_size; gateway listeners default to 2 MiB (#18882, #19518). - Updated HTTP and WebSocket parsers are stricter, and the HTTP client now applies response header and frame size limits by default. Verify integrations that communicate with legacy or non-compliant HTTP or WebSocket services after the upgrade. The REST API also rejects requests with more than 100 query parameters (#19694).
- MQTT connectors and cluster links now interpret
retry_intervalin seconds as configured instead of multiplying it by 1000, so QoS 1 and QoS 2 retransmissions now use the configured interval (#19885). - The RabbitMQ connector no longer multiplies
heartbeatby 1000, so an existing value now means what it says (#19910). - An outbound TLS client that sets
verify_peerwith nocacertfilenow usesnode.trusted_certs, and the built-in HTTP client no longer carries a CA bundle of its own (#19756). - The Datalayers Arrow Flight driver now verifies the server certificate and rejects
verify_none(#19815). - A TLS listener now refuses new connections while a large burst of handshakes waits to start (#19940).
- The QUIC listener setting
ssl_options.hibernate_afteris deprecated and has no effect, and QUIC connections now followmqtt.hibernate_after(#19407). - The
socketTCP backend now releases receive buffers between packets, set by the newtcp_options.buffer_retention_strategydefault ofpacket(#19785). recv_cntandsend_cntnow count MQTT packets instead of TCP packets (#19790).GET /clientsno longer returns theseqno_q1_*andseqno_q2_*fields of a durable session (#19651).- Only global administrators and global viewers can read the audit log (#18883).
topic_matchnow returnstruewhen a filter that ends with#is matched against an invalid topic such as#/a(#19710).- A plugin that reads the client information table directly no longer finds
sock,clientidorusernameunderconninfo(#19419, #19761). - A plugin or hook that reads the in-memory session header
deliver_begin_atmust usedeliver_begin_at_us, which is in microseconds (#19774).
Enhancements
Security
- #19756 Added the
node.trusted_certsconfiguration. It selects the CA certificates that outbound TLS clients trust when their own TLS configuration names no CA. The defaultsystem_defaultuses the CA bundle file of the operating system. Setfile:///absolute/pathto use a different PEM file.
An outbound TLS client that sets verify = verify_peer and leaves cacertfile empty now verifies the server against these CA certificates. Before, such a client failed to connect.
The built-in HTTP client that some integrations use (for example AI Completion, ClickHouse, OpenTSDB, TDengine, Tablestore, HTTP authentication and authorization, and S3) no longer has a CA bundle of its own. It uses node.trusted_certs too.
An outbound TLS client can also set cacertfile = system_default to trust the CA certificates of node.trusted_certs. When node.trusted_certs finds no CA bundle file, the client uses the CA certificates that Erlang/OTP loads from the operating system. Listeners do not accept this value.
The Docker image now installs the current Mozilla CA bundle from curl.se at build time.
#19702 also updated the bundled CA trust store to the Mozilla CA set as of 2026-10-20.
The bundle now holds 118 certificates. Relative to the previous bundle it drops emSign Root CA - C1,
emSign ECC Root CA - C3, emSign ECC Root CA - G3, SecureSign Root CA12 and Atos TrustedRoot 2011, and it
adds SECOM TLS RSA Root CA 2024, SECOM TLS ECC Root CA 2024, Telia RSA TLS Root CA v3 and
Telia EC TLS Root CA v3.
This bundle is the trust store for connectors that send HTTP requests and have no cacertfile configured. Set
cacertfile to pin the trust anchors of an outgoing connection yourself.
- #19088 The
authentication_settings.ignore_backend_failuresandauthorization.ignore_backend_failuressettings now apply in both thelegacyandhardenedsecurity profiles. Previously they took effect only under thehardenedprofile.
Each setting now accepts three values:
per_security_profile(the new default): Follow the security profile. Thelegacyprofile ignores backend failures. Thehardenedprofile denies the client.true: Ignore backend failures and continue to the next authenticator or authorization source.false: Deny the client when a backend fails.
Upgrade note: under the legacy profile, a stored value of false now denies the client on a backend failure. Saving the authorization settings through the REST API or the Dashboard in an earlier 6.x release can store false even if you did not change this setting. To keep the previous legacy behavior, set the value to per_security_profile after the upgrade.
- #19096 Added the
mqtt.max_connect_packet_sizesetting. It limits the size of a CONNECT packet, in addition tomqtt.max_packet_size. EMQX closes the connection of a client whose CONNECT packet is larger, and counts it under the listener shutdown counterconnect_packet_too_large. The default is1MB, the same as the defaultmqtt.max_packet_size, so nothing changes until the setting is lowered.
Performance and Resource Usage
- #19642 Added the listener option
tcp_options.low_watermarkfor thegen_tcpTCP backend.
When the Erlang socket buffer of a connection grows beyond high_watermark, the socket becomes busy and stays busy until the buffer drains below low_watermark. Before this change, low_watermark was always the Erlang/OTP default of 4KB, so a connection that went busy at the default 1MB high_watermark stayed busy until almost all queued data was sent.
When the option is not set, it defaults to a quarter of high_watermark, but never below 4KB. This is the same ratio the socket TCP backend uses to clear congestion; that backend ignores the option. A value above high_watermark is clamped to high_watermark.
-
#19240 Reduced temporary memory allocation when receiving MQTT PUBLISH messages, without changing packet validation.
-
#19256 Reduced the memory used by each TLS connection: the per-connection ETS table for shared socket options is gone, and idle TLS 1.3 connections release their cipher state.
-
#19277 Reduced the memory used by each MQTT connection. Connections in the same zone now share one copy of the packet parser, serializer and zone settings instead of each keeping its own. With 10,000 idle TCP connections this saves about 170 bytes per connection.
-
#19278 Reduced memory usage for MQTT connections and external subscriptions.
-
#19419 Reduced the memory each connection occupies in the client information table. The table no longer stores the connection's socket term under
conninfo.sock. Each entry is 16 bytes smaller for a TCP connection with thegen_tcpbackend, 32 bytes smaller with thesocketbackend, and about 128 bytes smaller for a TLS connection.
This changes the stored copy only. Hook callbacks receive the connection's own conninfo, which still carries sock. REST API responses are unchanged. Plugins that read the table directly, through emqx_cm:get_chan_info/1,2, emqx_cm:lookup_client/1, emqx_mgmt:lookup_client/2, emqx_channel:info/1 or emqx_connection:info/1, no longer see sock under conninfo. Use emqx_channel:info(conninfo, Channel) in the connection process to read it.
-
#19420 Each TLS handshake no longer makes two calls to the file server to resolve the
cacertfilepath. Under a burst of TLS connections this removes the message backlog on thefile_server_2process. -
#19514 Reduced the CPU cost of handling PUBLISH packets.
-
Topic splitting and topic validation do less work for each message.
- The Sparkplug B alias-mapping hook checks the topic first. It does no more work for topics outside the Sparkplug B namespace.
- The A2A registry, schema validation, message transformation and legacy topic metrics add their message hooks only when the feature is first used. Before that, they add no cost to each message.
- When no rate limit is configured for a client, the limiter configuration is not checked on each publish.
In a test with 60,000 clients that each publish one 1 KB QoS 0 message per second, broker CPU usage dropped with both the gen_tcp and the socket backend.
-
#19554 Reduced memory retained by oversized socket buffers reused for smaller network reads. The default Erlang distribution and
gen_rpcsocket buffer sizes have been increased to 1088 KiB, above Erlang's reusable buffer limit. -
#19556 A write to a cluster table now runs in a helper process only when the calling process has 100 or more messages queued, instead of 10. Under a high rate of new subscriptions, the broker workers that store routes crossed the old limit on one write in ten and waited for the helper each time, which added to SUBSCRIBE latency.
-
#19627 Connections on a TCP listener with the
gen_tcpbackend no longer wait for the socket driver on every outgoing packet. This lowers broker CPU usage per message. In a 1-to-1 publish/subscribe benchmark with QoS 0, CPU usage per message was about 12% lower. -
#19651 Keep the per-connection stats in an atomics array owned by the connection process instead of writing them to the
emqx_channel_infotable.
Each row of the table now holds a reference to the array. The packet and message counters are incremented in place, so GET /clients, GET /clients/{clientid} and emqx ctl clients show report them as they happen instead of up to one stats interval late. The row is no longer rewritten after each SUBACK and on every stats timer tick, which removes a decompress-and-recompress of the row on each of those writes, and the counters no longer live on the heap of the connection process.
The seven seqno_q1_* and seqno_q2_* fields of a durable session, which GET /clients and GET /clients/{clientid} showed with no consumer, are removed from the response. n_streams stays.
-
#19710 Topic matching no longer splits the topic into levels in these places:
-
shared-subscription topic matching;
- the
topic_matchfunction of rule engine SQL and variform expressions; - the route updates of cluster links.
The result is the same as before for a valid topic. The topic_match function takes runtime data, and a topic with a # level that is not the last level is not a valid topic. Such a topic now matches a filter with a trailing # where it did not match before. For example, topic_match('#/a', '#') now returns true.
The default acl.conf now compares the dashboard username with an equality check instead of a regular expression.
-
#19718 An idle MQTT connection no longer wakes from hibernation at every keepalive check. It now wakes once, when the client is late by the keepalive deadline. The time to detect a client that is gone is unchanged. With many idle clients, the broker uses less CPU.
-
#19719 Reduced memory usage for in-memory sessions and the amount of session data copied during garbage collection.
-
#19732 Message publishing no longer calls into cluster linking on nodes where no cluster link is enabled.
-
#19733 Message delivery no longer reads the listener rate limiter configuration for each message when the listener has no delivery rate limit.
A delivery rate limit that is added to a listener at runtime now applies to an existing client session within one statistics interval (mqtt.idle_timeout, 15 seconds by default), instead of on the next delivered message. A new client session is limited from its first delivered message, as before.
- #19739 The broker no longer builds and publishes the
$SYS/brokers/<node>/clients/<clientid>/connectedand.../disconnectedmessages when nothing can receive them: no client in the cluster is subscribed to the topic, and the rule engine ignores$SYSmessages (rule_engine.ignore_sys_message = true, the default). This saves about 4% of the broker CPU spent per connection.
A subscriber on the topic still receives the event. With rule_engine.ignore_sys_message = false the messages are published as before, so a rule that selects from a $SYS topic still sees them. The same applies to the subscribed and unsubscribed client events, which are off by default.
-
#19740 Reduced the CPU cost of each client connection on a single-node deployment by about 7%. The session lock that EMQX takes on each CONNECT no longer sends an internal remote call to the local node when the cluster has only one node.
-
#19761 Reduced the memory each connection occupies in the client information table by the length of its client identifier and username.
The table no longer stores the clientid and username entries under conninfo when they are
equal to the ones under clientinfo, which it keeps. They still appear under conninfo when the
CONNECT packet values differ from the ones the client ended up with, for example when the broker
assigned the client identifier.
This changes the stored copy only. Hook callbacks receive the connection's own connection information, which still carries both fields, and REST API responses are unchanged.
- #19774 Reduced memory allocation and CPU overhead when delivering QoS 1 and QoS 2 messages through in-memory sessions.
During rolling upgrades, inflight-message REST API pagination has two limitations:
- Older nodes reject pagination cursors with HTTP 400 if successive REST requests switch from a 6.3.3 node to an older node.
- After session takeover from a 6.3.3 node to an older node, inflight messages with the same microsecond delivery timestamp can be skipped if they span multiple pages.
MQTT message delivery is unaffected.
- #19785 Reduced memory retained by TCP connections using the
socketbackend. The newtcp_options.buffer_retention_strategysetting defaults topacket, which releases receive buffers while waiting for the next MQTT packet. Set it toalwaysto keep the previous buffer retention behavior.
Reads with a known packet length are limited to 16 MiB each. Larger packets are received in multiple reads.
- #19790 Removed the count of socket receive and send operations from the per-client statistics.
The per-client recv_cnt and send_cnt fields now show the number of MQTT packets received and sent, the same values as recv_pkt and send_pkt. Before, they showed the number of TCP packets. This applies to the /clients REST API, the emqx ctl clients stats CSV export, and the stored information of durable sessions. The fields are kept for compatibility.
-
#19800 Reduced CPU overhead and memory allocations when delivering MQTT messages to subscribers.
-
#19810 Reduced CPU overhead and temporary memory allocations when processing incoming MQTT messages.
-
#19811 Reduced the CPU cost of authorization checks with the file (ACL) authorization source and other rule-based sources. A check splits the topic into levels at most once, instead of once for each topic filter of the rules. A topic filter that starts with a fixed level, such as
$SYS/#, causes no split when the topic starts with a different level.
MQTT Listeners
- #19407 A QUIC connection now takes its hibernation timeout from
mqtt.hibernate_afterof the listener's zone, the same as a TCP or SSL connection. The control stream and the data streams of the connection both use this value.
The QUIC listener setting listeners.quic.<name>.ssl_options.hibernate_after is deprecated. EMQX still accepts it in the configuration, but it has no effect. Before this change, the QUIC listener setting was in use, and a change of mqtt.hibernate_after had no effect on QUIC connections.
Both settings default to 5s. When neither setting was changed, the hibernation behavior of QUIC connections does not change.
Clustering
- #19242 Added the
rpc.compress_levelandrpc.compress_thresholdsettings to compress the data that cluster nodes send to each other, including forwarded MQTT messages.
rpc.compress_level sets the zlib level, from 1 (fastest) to 9 (smallest output). The default is 0, which disables compression, so nothing changes on upgrade. rpc.compress_threshold sets the minimum packet size to compress and defaults to 1KB.
Compression uses CPU on both the sending and the receiving node to reduce the traffic between them. Enable it when the bandwidth between nodes is limited or billed, for example when a cluster spans availability zones. Level 1 gives most of the size reduction at the lowest CPU cost. For JSON payloads of 0.5 to 1 KB, level 1 reduced the traffic between nodes by about 60% and raised CPU usage by about 10% on the sending node and 5% on the receiving node. Payloads that are already compressed or encrypted do not get smaller, so for them compression only adds CPU cost.
Durable Storage, Message Queues and Streams
-
#19217 Removed the 49.71-day upper limit on regular-message retention periods for Message Queues and Message Streams.
-
#19623 Improved Message Queue replay performance by skipping stored records older than the retention period when consumers start or recreate iterators. Saved acknowledgements are preserved.
-
#19624 Added overload protection to the durable-storage write path. When the optimistic transaction manager reaches its configured message-queue limit, EMQX aborts queued transactions with a recoverable
overloadederror. Callers can retry these transactions. EMQX also rejects transactions that exceed the configured maximum number of operations.
Data Integration
-
#18926 Added IPv6 support to the Kafka, Confluent and Azure Event Hubs connectors.
-
The connectors can connect to brokers at bracketed IPv6 addresses in
bootstrap_hosts, for example[::1]:9092or[fd00::5]:9092,host2:9093. - The connectors can reach hostnames that resolve only to IPv6 addresses, and brokers that advertise IPv6 addresses.
- The new
socket_opts.ip_familyoption selects the IP address family. With the defaultauto, a hostname is tried over IPv4 first and then over IPv6. Set it toipv6to connect over IPv6 only, or toipv4to connect over IPv4 only.
The upgraded Kafka client library also fixes a sync produce timeout. It could happen when SASL re-authentication ran while requests were still pending.
- #19204 Added AWS IAM role credential support to Amazon Kinesis connectors.
When both the access key ID and secret access key are omitted, EMQX obtains temporary credentials from an ECS task role or EC2 instance metadata and refreshes them before they expire.
- #19544 Added the optional
regionfield to the S3 connector and to the S3 exporter of file transfer. When it is set, requests and presigned URLs are signed with this region instead of the region inferred from the endpoint host.
When region is not set, the region is now correctly inferred from AWS S3 interface VPC endpoint (PrivateLink) hosts, for example bucket.vpce-1a2b3c4d-5e6f.s3.us-west-2.vpce.amazonaws.com. Before this change, requests to such endpoints were signed with us-east-1, and AWS rejected requests to endpoints in other regions.
- #19815 The
arrow_flightdriver in the Datalayers connector now usesdatalayers-adapter-erl0.1.6.
Arrow Flight TLS connections verify the Datalayers server certificate. They default to
cacertfile = "system_default" and verify = verify_peer. The system_default value uses the CA
bundle from node.trusted_certs. A PEM CA path can replace the default. The connector
rejects verify_none, client certificate settings, and managed_certs.
The default influxdb_v1 driver keeps its existing TLS behavior.
Plugins and Extensions
- #17917 Added a new plugin
emqx_bcastthat provides product-level device messaging APIs:PubBroadcast(broadcast to all online devices of a product),BatchPub(batch delivery to a device list with offline storage and QoS 1 acknowledgement tracking), andRegisterMessage(message pre-registration with content deduplication and TTL refresh).
The plugin exposes a unified endpoint POST /api/v5/plugin_api/emqx_bcast/pub, delivers messages directly to connected client channels, replays pending QoS 1 deliveries on device reconnect, and exports Prometheus metrics at GET /api/v5/plugin_api/emqx_bcast/metrics.
Plugin restart, index-owner handover, and core shutdown preserve queued delivery progress, per-device publication order, and the QoS 1 delivery ledger without requiring clients to republish or resubscribe.
- #18843 Added a new API scope,
plugin_api, that grants access to the plugin-extended API endpoints (/plugin_api/...) and nothing else.
Assign it to an API key or a Dashboard user to let them call the endpoints a plugin publishes, without granting them the system scope, which is administrator-equivalent. The plugin_api scope does not grant plugin installation, start, stop or configuration — those stay in the system scope.
Existing API keys and Dashboard users that hold the system scope keep their access to the plugin-extended API endpoints.
-
#19126 Added support for publish hooks to report successful message persistence. The HTTP publish API returns HTTP 200 with the message ID when a plugin reports persistence, even when no subscription matches.
-
#19359 Added the
node.pinned_pluginssetting. It lists plugins that a node runs independently of the cluster plugin config, for example plugins that are built into a container image.
Set it to a comma-separated list of name-vsns, for example EMQX_NODE__PINNED_PLUGINS="my_plugin-1.0.0", or to an array. A pinned plugin runs from the directory <plugins.install_dir>/<name-vsn>, which must already hold the extracted package, for example in the container image. It is always enabled, and needs no plugins.states entry, no plugins allow grant and no install command. Its files and config are never copied from or to other nodes. Its config is the package default, merged with etc/plugins/<name>.hocon when that file exists. The config can be changed through the API or the Dashboard like for other plugins. A saved config is stored in data/plugins/<name>/config.hocon, and from then on that file is the whole config. The change applies on the nodes that run that plugin version. The plugin's own API endpoints under /api/v5/plugin_api/ work as for other plugins. The node ignores plugins.states entries for the same plugin name, and cluster-wide plugin operations from other nodes skip it. When a pinned plugin directory is missing or the plugin fails to start, the node logs an error, raises an alarm, and keeps running.
GET /api/v5/plugins and GET /api/v5/plugins/{name} return a pinned flag for each plugin and for each node in running_status. Only the CLI can stop and start a pinned plugin, with emqx ctl plugins stop|start|restart on the node that pins it. A stop lasts until the node starts all its plugins again, at boot or after a cluster join. On the node that pins a plugin, the API returns HTTP 409 with the code PLUGIN_PINNED for the plugin's lifecycle operations: install, uninstall, start, stop, boot order change and cluster sync.
Nodes can now run different versions of the same plugin. The plugin must then stay compatible across these versions, for example in shared database tables, in RPC between its instances, and in its config.
Bug Fixes
Security
-
#16389 Fixed an issue where the OAuth2
client_secretwas returned in plaintext by the configuration read APIs for authentication, authorization, gateways and HTTP connectors. It is now masked in API responses, and re-submitting the masked value keeps the stored secret unchanged. -
#18810 Dashboard single sign-on now completes an OIDC or SAML login only in the browser that started it. EMQX gives each login a value, keeps it in a browser cookie, and matches it when the identity provider sends the browser back. A login that arrives in a different browser signs no one in.
Three notes for operators:
- Open the Dashboard at the address set in
dashboard_addrbefore starting an SSO login. The identity provider sends the browser back to that address, and the cookie belongs to it. - SAML single sign-on now needs an
httpsaddress indashboard_addr. A SAML identity provider sends the browser back with a cross-site form post, and browsers carry the cookie on such a post only over HTTPS. -
If browsers in your setup cannot keep the cookie and SSO login fails, set
skip_login_cookie_check = trueon the OIDC or SAML backend. This turns the browser check off for that backend, so enable it at your own risk. -
#18879 Audit records for requests denied by role-based access control now include the authenticated dashboard user or API key and the request's path parameters.
Before this change, a request that passed authentication but was denied by role-based access control was recorded with an empty source and without http_request.bindings, so GET /api/v5/audit could not show who made the request or what it targeted. Records for unauthenticated requests are unchanged.
-
#18883 Restricted the audit log to global users. The audit log records operations from every namespace, so only global administrators and global viewers can read it through
GET /api/v5/audit. Dashboard users and API keys that belong to a namespace can no longer read it. -
#18964 Fixed the SCRAM HTTP authentication backend rejecting the OAuth2 configuration that the Dashboard submits for it. Creating an
SCRAM+HTTP Serverauthenticator with OAuth2 enabled now succeeds, and the access token is sent as aBearerauthorization header on the user lookup request. -
#19036 Fixed a credential leak in gateway logs. When a gateway connection terminated during startup, for example after a failed DTLS handshake, the supervisor offender report included the full connection arguments, exposing
clientinfo_override.passwordand the gateway authentication configuration. These values are no longer logged. -
#19037 Gateway debug logs no longer contain raw datagrams:
received_data,received_udp_proxy_dataandsend_datanow log the byte size only, because the encoded packet may carry credentials. The parsed packet is still logged with protocol-level redaction.
The CoAP Gateway additionally redacts the session token issued on
POST /mqtt/connection and credentials sent with the short query aliases
(t, p), both in the parsed packet logs and in the log of rejected
requests.
-
#19098 Hardened the configuration responses of
GET /api/v5/schema_registry,GET /api/v5/schema_registry/:nameandGET /api/v5/opentelemetryby masking credential-bearing values in the returned configuration. The corresponding create and update endpoints accept the masked values back without overwriting the stored ones. -
#19125 Fixed inconsistent default scopes for namespaced Dashboard users and API keys.
-
A namespaced administrator with no explicit scope list got the scopes of a global administrator. This happened when the user was created or updated with
"scopes": "unset", or when an update sent back the default scope list unchanged. The user now gets the namespaced administrator defaults. - A namespaced viewer, created without
scopes, got more scopes than a namespaced administrator:gateways,publishandaudit. A namespaced viewer, and a namespaced viewer API key, now defaults to the same management scopes as a namespaced administrator, without the login-only scopes. A namespaced viewer can no longer be givengateways,publish,auditormfa_management. -
At startup, EMQX removes
gateways,publish,auditandmfa_managementfrom the stored scope lists of existing namespaced viewers. Such a viewer loses read access to the gateway endpoints. Thepublishandauditendpoints were already denied to namespaced users. Stored scope lists of existing API keys are not changed. -
#19142 Fixed an issue where the Gateway lookup CLI command exposed authentication credentials and client information override passwords. Sensitive Gateway configuration values are now redacted from the lookup output.
-
#19175 Removed the RPC insecure authentication fallback.
Cluster nodes now always authenticate their backplane (RPC) connections with a challenge-response handshake. The old fallback, which sent the Erlang cookie to the peer when the handshake failed, has been removed.
The rpc.insecure_fallback config key no longer has any effect. It existed to cluster with EMQX
releases older than 5.3.0, which do not speak the challenge-response handshake. A config file that
still sets the key keeps loading, and the value is ignored.
-
#19254 Only the global administrator can now export the configuration as
text/plainfromGET /api/v5/configs. Other dashboard users and API keys, including viewers and namespaced users, get a403response. This export is not fully redacted and pairs withPUT /api/v5/configs. The redactedapplication/jsonvariant of the same endpoint is unchanged. -
#19294 Viewers can no longer download backup files. This applies to Dashboard users and API keys with the viewer role, both global and namespaced, and to global viewers that pass the
namespacequery parameter. They now get HTTP 403 fromGET /api/v5/data/files/:filename. Viewers can still list backup files. Administrators are not affected. -
#19600 Fixed client certificate information being lost when a proxy terminates mutual TLS and re-encrypts the connection to EMQX. When a listener has
proxy_protocol = trueand the PROXY protocol v2 header carries SSL TLVs, EMQX now reads the client certificate details from the header. It reads the certificate from its own TLS session only when the header carries no certificate information. Before this change, a listener with TLS enabled always read its own TLS session, so a deployment that forwards the client common name through HAProxy lost it after an upgrade from 5.x.
Also fixed several listener socket option problems: v6only and {tune_buffer, false} are now handled correctly, invalid socket tuning options are rejected instead of applied, and a fragmented PROXY protocol v1 header is now rejected instead of being parsed incorrectly.
- #19694 Hardened HTTP and WebSocket handling by updating the
gunandcowlibdependencies. The HTTP/1.1 and WebSocket parsers are stricter and the HTTP client applies response header and frame size limits by default, which can break integrations that communicate with legacy or non-compliant HTTP or WebSocket services; verify such integrations after upgrading.
The REST API now rejects requests with more than 100 query parameters, and the limit is documented in the generated API specification.
-
#19803 Fixed an issue where GCP PubSub Producer request bodies were not properly redacted from logs and traces. When such bodies were large enough, they could lead to increased CPU and memory usage, as well as
long_scheduleandlarge_heapwarnings pointing toehttpcprocesses formatting log messages. -
#19884 In the
jqfunction of the rule engine SQL,envand$ENVnow evaluate to an empty object.
MQTT Protocol and Listeners
-
#18882 Aligned the WebSocket message size limit of MQTT and gateway listeners with the configured protocol limits.
-
On MQTT WebSocket listeners,
websocket.max_frame_size = infinity(the default) now means twice the zone'smqtt.max_packet_size. Set an explicit value when clients send several MQTT packets in one WebSocket message. - On gateway WebSocket listeners,
websocket.max_frame_sizenow takes effect. Its default is 2097152 (2 MiB), and the valueinfinityselects this default. websocket.max_frame_sizeaccepts values up to 268435455.
The limit applies to a WebSocket message after its fragments are joined, and after decompression when compress is enabled. When a client sends a larger message, the connection is closed with WebSocket status code 1009.
-
#19174 With
parse_unit = frame(the default), TCP and SSL listeners now applymqtt.max_connect_packet_sizeat the transport: a first packet larger than the limit is refused from its fixed header, before its body is buffered. Previously the transport buffered up tomqtt.max_packet_sizebefore the check ran. The limit goes back tomqtt.max_packet_sizeonce the client is connected. Defaults are unchanged (both limits are1MB), so nothing changes untilmqtt.max_connect_packet_sizeis lowered. -
#19239 Fixed the WebSocket listener response for an unparseable
Sec-WebSocket-Protocolheader. Such a request now gets the same handling as a request without the header (a 400 reply whenfail_if_no_subprotocolis enabled, an upgrade without subprotocol otherwise), in the WebSocket listener, the gateway WebSocket behavior and the OCPP gateway. Previously, the request process exited, the client got an empty 400 response, and a crash report was written to the log. -
#19374 Fixed an issue where an abnormal WebSocket disconnect did not trigger publication of a pending MQTT 5 Will Message when the Session Expiry Interval was 0.
-
#19511 Fixed the handling of MQTT packets that a client sends after DISCONNECT on the same connection. The broker now ignores them. Before this fix, a CONNECT sent right after DISCONNECT could make the broker log the error
session_stepdown_request_exceptionand drop the session of a client that connected with Clean Session set to false. -
#19518 Hardened the WebSocket message size limit of MQTT and gateway listeners so that it also covers compressed messages.
-
#19522 A client connection that is closed by the
force_shutdownpolicy, because its message queue or heap is too large, no longer produces an error-level supervisor report. The listener shutdown counters now count these closures undermailbox_overfloworproc_heap_too_large. EMQX still logs each closure at warning level. -
#19531 A TLS alert that closes an established connection no longer produces an error-level supervisor report. Each alert now increases a listener shutdown counter named by the alert kind, for example
user_canceledorbad_record_mac, and is still logged, at notice level. Alert codes that the TLS stack does not recognize share one counter,unsupported/unknown_alert. Other socket errors that previously produced the same error report, such as an invalid proxy protocol header, are counted assock_errorand logged at notice level. The counters are shown byemqx ctl listenersand in theemqx_client_disconnected_reasonPrometheus metric. -
#19574 Fixed publish-hook errors when client IDs containing MQTT wildcards were included in
$SYSclient event topics. The characters#,+, and%in client IDs are now encoded as%23,%2B, and%25in these topic names. Client IDs in event payloads remain unchanged. -
#19632 Upgraded the ranch library to 2.2.0-emqx-4 to improve TLS connection handling in WSS listeners.
-
#19640 Fixed MQTT packet ordering over WebSocket when a message contains complete packets followed by an incomplete or invalid packet, and prevented connection process crashes when handling such invalid packets.
Fixed unnecessary error logs when sessions expire after node evacuation or rebalancing.
-
#19649 Fixed
cert_file_not_founderror logs emitted for TLS listeners withssl_options.cacertfileset to an empty string. -
#19674 Fixed
ssl_options.handshake_timeouthaving no effect on WSS listeners. A WSS listener always used a TLS handshake timeout of 15 seconds. It now uses the configured value. -
#19737 Fixed an issue where shared subscribers could stop receiving messages after other subscribers to the same topic filter unsubscribed or disconnected.
-
#19940 Fixed an issue where a large burst of TLS connection attempts could make a TLS listener stop accepting connections. Clients reported connect timeouts, the node used more and more memory, and the listener did not recover while clients continued to try to connect, so a restart was usually necessary.
EMQX now closes some new TLS connections immediately while such a burst is in progress. The listener continues to accept other connections, and the refused clients succeed when they try again. Each refused connection is counted under the reason too_many_handshakes, which is visible with emqx ctl listeners and in the emqx_client_disconnected_reason metric.
- #19988 Fixed
ssl_options.gc_after_handshakeon SSL listeners, which had no effect since 6.1.0. When it is enabled, EMQX again runs a garbage collection on the TLS connection processes once the handshake is complete, which reduces the memory used by each TLS connection.
Clustering
-
#19136 Fixed an issue where a cluster link could drop the messages in flight when the connection to the peer cluster was lost. Failures that mean the connection is gone or was never established (for example a connect timeout, a DNS resolution failure, or a transport error) are now treated as recoverable, so the affected messages are retried until the request expires instead of being acknowledged and counted as failed.
-
#19139 The cluster link health check now verifies the connection to the remote broker end to end: it sends an MQTT
PINGREQand waits for thePINGRESPinstead of only inspecting the local client state. A link whose packets are silently dropped is reported asdisconnectedas soon as the probe goes unanswered, so its buffer workers keep the messages queued and the resource is restarted, instead of the link stayingconnecteduntil the client's own keepalive noticed the silence (1 to 2 xkeepalive, 60 s by default), which ongoing traffic could skip altogether. The message forwarding clients of a link are no longer configured with a client keepalive, as the health check replaces it. -
#19556 Fixed etcd-based cluster discovery to survive a transient etcd outage. The node now reconnects to etcd with a backoff, and restores its own registration key after the connection comes back. Before this change the discovery process failed when etcd became unreachable, and the node stayed unregistered after etcd recovered.
-
#19991 Updated gen_rpc to 4.0.1.
Data Integration
- #18945 Fixed the RocketMQ action's handling of a topic that does not exist on the broker.
When the topic had no route on the name server and the broker did not auto-create topics (for example an Alibaba Cloud instance where topics are created per namespace in the console), every message was dropped with a case_clause error, and the whole message was written to the log each time. Now:
- An action whose topic contains no placeholders is checked on every health check. When the topic does not exist, the action reports
disconnectedwith a message that names the topic and the namespace, messages are held in the buffer and retried, and the action recovers on the first health check after the topic is created. - A message whose templated topic does not exist is dropped with a readable
topic_not_founderror, and the log no longer contains the message payload. - With a namespace configured, the auto-create default topic
TBW102was requested as<namespace>%TBW102, which never exists, so topics could not be auto-created in a namespace. The default topic is now requested by its bare name, as the Java client does.
Also fixed the connector alarm that was raised and cleared every two minutes while no producer was running: the name server closes an idle connection after 120 seconds, and the client now reconnects right away instead of reporting connecting for one health check.
- #18987 Despite having the
resource_opts.health_check_timeout, some Connectors/Actions/Sources did not actually use that value for timing out health checks. This has been fixed.
The affected integrations were:
- Cassandra Connector
- DynamoDB Connector
- GCP PubSub Consumer Source
- IoTDB Connector (Thrift driver)
- Snowflake Aggregated Connector
- SQLServer Connector
- TDEngine Connector
- MySQL Connector
- Doris Connector
-
Postgres Connector
-
#19055 On some occasions, Connectors would fail to report more detailed connection failure reasons on their first health checks, reporting only "resource down: disconnected". Now, they will report the last captured error. Affected connectors: PostgreSQL family, MySQL, Redis, Cassandra, TDengine, SQL Server, IoTDB, Couchbase, DynamoDB, Snowflake aggregated, Syskeeper.
-
#19099 Improved some SQL functions in the rule engine so that each rule can only access the data written by rules in the same namespace. Rules in the global namespace keep sharing the global data space.
-
#19108 On rare occasions, under heavy load, the GCP PubSub Producer and HTTP Actions could report unrecoverable errors with the reason
{error,closed}. These errors are now deemed recoverable, as they happen as the remote server closes connections. -
#19109 Improved the returned details when Connector and Action/Source health checks fail.
Affected Connectors/Actions/Sources:
- Disk log
- GreptimeDB
- HTTP
- InfluxDB
- Kafka Consumer
- Kafka Producer family
- OpenTS
- Pulsar Producer
- Redis
- RocketMQ
- Syskeeper
-
Tablestore
-
#19117 Messages buffered by an
mqttaction are no longer lost when the remote broker is unreachable: connection-level failures (for examplessl_closed,econnrefused, keepalive or socket timeouts and DNS errors) are now retriable, so the queries stay in the buffer untilrequest_ttlexpires instead of being counted asfailed(with fallback actions configured, such a request now triggers the fallback when it expires instead of immediately). -
#19122 The
mqttconnector health check now verifies the connection to the remote broker end to end: it sends an MQTTPINGREQand waits for thePINGRESP, bounded by half of the configuredresource_opts.health_check_timeout, instead of only inspecting the local client state. A remote broker that keeps the TCP connection open while packets are dropped is reported asdisconnectedas soon as the probe goes unanswered, so the resource is recreated and the action's buffer workers keep the messages queued instead of handing them to a connection that cannot deliver them; the client's own keepalive is no longer what notices the silence (1 to 2 xkeepalive, 160 s by default).
Connector status reasons no longer explain a connection closed by the peer as a listener capacity problem.
- #19186 When the S3 server responded with a non-successful HTTP status and provided no body, there could be a crash log message instead of a friendly error message in the status of a Connector/Action and in the logs.
e.g.:
2026-09-23T17:08:36.310622+00:00 [warning] msg: s3_http_request_partial_fail, pid: <0.5904.0>, reason: req_not_found, status: 307, ...
[error] tag: ERROR, msg: resource_exception, ..., stacktrace => [{unicode,characters_to_list,[undefined],...
-
#19222 Previously, when checking its health status, Pulsar Connector would always use a 5 s timeout when doing an internal call. Now, it uses the configured
resource_opts.health_check_timeout(default: 10 s), which allows more fine tuning if necessary. -
#19249 Despite having the
resource_opts.health_check_timeout, some Connectors/Actions/Sources did not actually use that value for timing out health checks. This has been fixed.
The affected integrations were:
- MongoDB Connector
-
Redis Connector
-
#19261 Enabled TCP keepalive on connections of HTTP-based connectors, such as HTTP Server, GCP PubSub, BigQuery, Couchbase and Snowflake, and of HTTP authentication and authorization.
These connections are opened with a 30-second keepalive, which used to have no effect. On Linux and macOS, an idle connection now sends a keepalive probe after 30 seconds and then every 5 seconds, and is closed after 3 unanswered probes. On other systems, keepalive is enabled with the OS timers.
- #19287 Fixed a GreptimeDB action failure caused by values that do not match the configured write syntax, such as a floating-point value in a field with the
isuffix. Invalid messages are now counted as failed without blocking the buffer worker. Valid messages in the same batch are still written.
Integer and unsigned-integer values and timestamps are validated against their supported ranges before writing. Integers without an i or u suffix are written as float64, following the InfluxDB line protocol.
- #19363 Despite having the
resource_opts.health_check_timeout, some Connectors/Actions/Sources did not actually use that value for timing out health checks. This has been fixed.
The affected integrations were:
-
Postgres Action and Connector
-
#19364 Previously, during the health check of Postgres Actions and their derivative families, a transaction to validate the table existence would remain active and holding a lock on the target table until the next health check. Now, the transaction is closed as soon as the table existence probe ends.
-
#19386 Despite having the
resource_opts.health_check_timeout, some Connectors/Actions/Sources did not actually use that value for timing out health checks. This has been fixed.
The affected integrations were:
-
Couchbase Connector
-
#19412 Previously, Sparkplug B metric alias mapping for
NDATAandDDATAwould not work properly when using topic mountpoints. Now they work in such situations as well. -
#19521 Fixed HTTP actions discarding messages on HTTP 502 and 504 responses or temporary DNS resolution failures (
nxdomain). These errors are now recoverable, allowing the action buffer to retry until the request TTL expires.
Fixed HTTP actions ignoring parameters.max_retries in synchronous and asynchronous query modes. The parameter now controls retries within each action query, independently of retries performed by the action buffer. Existing free retries for normal connection shutdown races remain unchanged.
-
#19533 Fixed HTTP connectors using OAuth2 client credentials continuing to send a rejected cached token after an HTTP 401 response. The connector now refreshes the token and retries the request once, sharing refreshes across concurrent requests without resetting the configured HTTP retry budget. Failed token refreshes remain recoverable for buffered actions. This also applies to HTTP authentication and authorization.
-
#19619 HTTP actions no longer retry recoverable HTTP responses immediately within an asynchronous query. Both connector callbacks return these errors to the action buffer, whose retries are controlled by
resume_intervalandrequest_ttl. -
#19647 Fixed an issue where the Direct Upload mode in the S3 Action would ignore custom HTTP headers from its configuration.
-
#19650 Fixed a potential leak where reconnect callback functions could accumulate over time for some Actions when their underlying connection would reconnect.
Affected Actions:
- Oracle
- Postgres family (TimescaleDB, MatrixDB, CockroachDB, AlloyDB, Redshift)
- Cassandra
- Datalayers Arrow Flight
Fixed an issue where, after an Oracle Action was deleted or its SQL template changed, the old SQL could still be sent to the database when the connection reconnected, which produced errors such as ORA-00904 in the logs. Reconnect callbacks of removed actions are now cleaned up. The Oracle connector now also reconnects its pool workers automatically and re-validates the configured SQL on every reconnect.
-
#19663 Fixed an issue where MongoDB authentication, authorization, and data integration ignored the overflow_ttl and overflow_check_period settings. During bursts of queries, extra MongoDB connections closed after each query even when overflow_ttl was greater than zero. EMQX now keeps these connections available for reuse until their idle timeout expires.
-
#19664 Fixed a leak in RocketMQ Action where the producer options used by a previous incarnation of an Action could persist even after recreating it.
-
#19669 Fixed an issue where QuasarDB connection pools would remain running even after stopping/removing their Connectors.
-
#19670 Fixed an issue where adding a second Tablestore action to a connector made the queries of the existing action fail with
channel_not_found. -
#19690 Fixed an issue where an unresponsive EMQX Tables backend could block file and log operations across the node, potentially making the node unresponsive.
-
#19703 Fixed an issue with SparkplugB metric alias mapping where publishing an
NDATAmessage before aDBIRTHmessage could have the device's metric alias mapping inherit some names from the node's mapping. -
#19778 Added the possibility of setting TLS options for multiple Connectors:
-
ClickHouse
- OpenTSDB
- TDengine
- Tablestore
For Tablestore, while the config schema already accepted TLS options, they were not passed down to the driver client. Now they are.
-
#19779 Added the option of specifying TLS options for obtaining the initial token of WIF authentication for GCP-based connectors (GCP Pub/Sub Producer and Consumer, BigQuery). The other tokens in the chain now always verify the Google endpoints' certificates using the CA bundle set in
node.trusted_certs, with only the first step having different TLS options as it is destined to an arbitrary endpoint. -
#19841 Fixed an issue where creating an S3 Action without providing the
resource_optsconfiguration key would result in a 500 HTTP error. -
#19845 Removed redundant MongoDB action health checks. Previously, each action repeated a check already performed by its connector, so the number of checks grew with the number of actions and could cause connection-pool checkout contention and
mongodb_connector_pool_checkout_timeouterrors. -
#19885 Fixed
retry_intervalhandling for MQTT connectors and cluster-link MQTT clients. The value was incorrectly multiplied by 1000, causing QoS 1 and QoS 2 retransmissions to be delayed much longer than configured. -
#19888 Fixed a possible race condition where the automatic add channel retry of a Connector's channel could keep repeating even after the channel managed to be added successfully.
Although no report of real occurrence is known at the time, it would manifest with the following log in a RabbitMQ Connector, for example:
tag: CONNECTOR/RABBITMQ, msg: add_channel_failed, reason: already_exists
- #19992 Fixed growing memory use in Kafka and Confluent producer actions when acknowledgments are disabled (
required_acks = none).
Gateways
-
#19027 Fixed the GB/T 32960 gateway to reject frames whose header VIN differs from the VIN authenticated at login, and to reject a vehicle login on an already established connection. Previously a connection authenticated as one vehicle could publish telemetry attributed to another vehicle, and could re-login to switch VIN while retaining the previous vehicle's session state.
-
#19059 Fixed inconsistent frame size limit enforcement in the STOMP, NATS and JT/T 808 gateways, bounded STOMP transaction usage, rejected non-positive STOMP transaction timeouts, and prevented LwM2M re-registration from reusing a session with a different client identity. The NATS gateway now limits control lines with
max_control_linewhile retainingmax_payload_sizefor message data and INFO.max_payload. -
#19176 Fixed NATS Gateway subject translation so NATS subjects cannot acquire MQTT wildcard or shared-subscription semantics, and MQTT topics with ambiguous token boundaries are not delivered to NATS subscribers.
Subjects containing /, +, or #, or starting with $share., $queue., or $exclusive., are rejected. These restrictions also apply to reply subjects in PUB and HPUB requests; reply subjects must not contain NATS wildcards.
NATS > subscriptions and JWT permissions now require at least one trailing subject token. Unsupported subjects in NATS User JWT permissions are rejected during connection authentication.
Queue-group names containing MQTT separators or wildcards are rejected, and the resulting subscription filter must remain consistent with the requested NATS subject.
JWT publish and subscribe permissions are matched directly using NATS subject semantics. Wildcard deny rules now cover subjects starting with $, preventing MQTT wildcard exclusions from bypassing JWT restrictions.
Publish JWT permissions are checked against the original frame subject before message ingress. EMQX ACL authorization applies to the resulting MQTT topic after ingress rewrites.
- #19179 Limited concurrent CoAP and LwM2M block-wise exchanges and retained payload bytes per session. Block1 uploads now receive 4.29 when the exchange limit is reached or 4.13 when the byte limit is reached. Temporarily blocked Observe notifications and LwM2M downlinks remain queued and resume when capacity becomes available.
Failed LwM2M transfers release their block-wise resources before the next queued command is sent. Active transfers report timeouts even when no commands are queued or an incoming device message has already removed their expired block-wise state. Queue-mode devices keep commands cached until their next activity. LwM2M transport timers trigger retransmissions and report command timeouts when retries are exhausted. State transitions cancel obsolete acknowledgement timers.
- #19546 Limited each LwM2M session to 1,000 pending downlink commands. When the queue is full, EMQX rejects new commands with a 4.29 response over MQTT while preserving accepted commands in FIFO order. EMQX reports the pending count and fixed limit through mqueue statistics. This prevents repeated commands for the same path and type from growing the command-record eviction queue indefinitely.
The LwM2M receive window now uses monotonic time so wall-clock adjustments cannot cause downlink commands to be sent to sleeping devices.
-
#19582 Fixed MQTT-SN connection rejection reasons reported to hooks and logs. Unknown return codes now report
unknown_errorinstead ofaccepted, andrejected_invaild_topic_idis corrected torejected_invalid_topic_id. Consumers matching the misspelled reason must use the corrected name. The return codes sent to clients remain unchanged. -
#19676 CoAP Observe clients can now retrieve subsequent Block2 blocks with a different Token for each request. Clients that reuse the Observe Token remain supported. Re-registering an observation for the same topic replaces its Token and subscription options. Pending notifications and cached blocks follow the current observation lifecycle. Updating subscription options through the management API keeps the client's observation active.
-
#19693 The CoAP gateway now rejects a publish to a topic that is not a valid topic name, and a subscribe to a topic that is not a valid topic filter, with a
4.00 Bad Requestresponse. For example, a publish toa/#or+/awas previously accepted. -
#19741 Fixed the STOMP gateway to send a
RECEIPTwhen a client commits a transaction with areceiptheader. WhenCOMMITincludes this header, receipts for commands in the transaction now arrive in command order, followed by theCOMMITreceipt. -
#19742 Gateway WebSocket keepalive checks now count protocol packets instead of bytes, consistent with TCP/UDP connections.
-
#19746 CoAP and LwM2M gateways now require positive blockwise
max_body_sizeandexchange_lifetimevalues. Invalid configuration requests are rejected without changing the saved configuration. Before upgrading, replace any zero or negative values with positive values. -
#19747 CoAP and LwM2M gateways now require a positive blockwise
max_total_sizevalue. Invalid configuration requests are rejected without changing the saved configuration. Before upgrading, replace any zero or negative values with a positive byte budget.
Dashboard and REST API
-
#19003 Hardened
PUT /api/v5/api_key/:nameso that it only updates the fields present in the request body. Previously a partial update could also rewrite fields it did not mention, so an administrator who was not paying close attention to every field could unintentionally change the permissions of the API key named in the request. Calling this endpoint already requires administrator privileges. -
#19085 Fixed the REST API response for a malformed
Authorizationheader. Such a request now gets a401response with theAUTHORIZATION_HEADER_ERRORcode, the same as a request without the header. Previously, the request process exited, the client got an empty400response, and a crash report was written to the log. -
#19116 Improved the error message when a REST API request sends data that is not text, where text is expected. An example is a certificate in DER format in a field that needs the PEM format.
EMQX now refuses the value, reports that it is not valid UTF-8 text, and gives its size. Before, the message showed the content of the value as a long list of numbers, and said that the value had the wrong type although the request did contain one.
A configuration field that accepts any value no longer stores such data. Before, it stored an internal error in place of the value.
-
#19228 Fixed an issue where some authenticated callers could not read the API documentation endpoints (
/api-docs/swagger.json,/api-spec.json,/api-spec.md,/api-spec.htmland/api-spec/:tag). -
Dashboard users and API keys with an explicit scope list can now read these endpoints, whatever scopes they hold.
- Dashboard users and API keys with the viewer role or a namespaced administrator role can now read these endpoints.
Requests without credentials still get HTTP 401.
-
#19435 When a REST API handler failed with an internal error, the HTTP 500 response and the warning log entry contained the arguments of the failed function. These arguments include the request headers and the request body. As a result, the value of the
authorizationheader (a bearer token, or an API key and secret) and request body fields could be written to the log. The response and the log now keep only the structure of these values. Header names and body field names stay visible, and each string value is replaced by its length. -
#19448 Fixed the protobuf bundle REST API to return HTTP 400 with the code
BAD_FORM_DATAand a message that names the field, when a form field is sent in the wrong form:bundleas a text field, orname,root_proto_fileordescriptionas an uploaded file. Before this fix, such a request toPOSTorPUT /api/v5/schema_registry_protobuf/bundlereturned HTTP 500, except fordescription, which returned HTTP 400 with an internal validation message. -
#19503 REST API requests with malformed UTF-8 or unpaired Unicode surrogate escapes in JSON bodies now return a fixed HTTP 400 error before schema validation. This avoids misleading configuration errors and oversized error responses. Valid Unicode, including emoji, remains supported.
-
#19520 Fixed an issue where repeatedly deleting a Dashboard LDAP SSO backend could cause subsequent delete requests or attempts to recreate the backend to fail with an internal error.
After a backend is deleted, further delete requests now consistently return HTTP 404, and the backend can be created again normally.
- #19565 A malformed
Authorizationheader now returns 401AUTHORIZATION_HEADER_ERRORinstead of a bare 400, and its content is no longer written to the error log.
Credentials that the HTTP specification cannot parse, such as Basic credentials without the : separator or a Bearer token with an invalid character, previously made the request process exit. The crash report that followed held the header value at error level, so a client with a formatting mistake wrote its own credential to the log, and an unauthenticated caller could add a log entry of its own choosing on every request.
Plugins and Extensions
-
#18957 Fixed a bug where uploading a plugin package from the Dashboard replied
ALREADY_INSTALLEDand refused to install it when the package tarball was already present in the plugin install directory but had not been unpacked. The upload now goes through the installation allow-list first, replying403 FORBIDDENwith theemqx ctl plugins allowinstruction when the package is not authorized. Leftovers of an interrupted or failed installation (a directory without a readablerelease.json, or a manifest whose declared applications have not been unpacked) are no longer mistaken for an installation either: they are purged and the uploaded package is unpacked into a clean directory. An installation whose applications are still loaded is never purged: such an upload is refused withplugin_is_in_useand the plugin must be stopped first. A plugin installation attempt which is refused or which fails no longer deletes the package file it replaced, so the local copy of the installed package survives a refused upload and can still be used to repair the installation. An upload is refused when the package file it would replace can not be read, and a node where the plugin is already completely installed keeps the package which matches its files. -
#19034 Hardened plugin package installation and loading.
-
#19035 Hardened plugin package installation and synchronization.
-
#19115 Hardened the plugin configuration read endpoints (
GET /api/v5/plugins/:name/configandGET /api/v5/plugins/:name/config/download) by limiting them to the global administrator. -
#19195 Hardened plugin package name validation.
-
#19339 Fixed installing a plugin package that bundles an application which EMQX already provides.
Such a package was refused with plugin_app_loaded_outside_package, for example the emqx_acme plugin, which bundles jose. The package now installs, and the plugin uses the copy of the application that EMQX has loaded, whatever version the package bundles. When the versions differ, EMQX logs a plugin_app_already_loaded warning. Stopping or uninstalling the plugin does not stop or unload the EMQX copy.
Plugin packages built with mix emqx.plugin no longer bundle the dependencies that EMQX provides.
-
#19358 Fixed plugin cluster synchronization (
POST /api/v5/plugins/cluster_sync) stopping the plugin version it was asked to keep. The named version now stays installed, enabled and running on every node, and only the other versions of the plugin are removed. -
#19373 Fixed an issue where starting a node rewrote the plugin states in the cluster configuration. A
plugins.statesvalue set only on one node, for example inetc/emqx.confor an environment variable, was published to all other nodes. Node startup also waited until every other node had applied the change, so a node could stay in thestartingstate while another node lagged behind. -
#19391 Improved the error message when a plugin upload is refused because another version of the same plugin is loaded. The message now names the loaded version and tells you to uninstall it or restart the node. Previously, the message described only the case where two different plugins bundle the same application, and did not say which plugin to act on.
-
#19433 Fixed the plugin config upload and plugin install REST APIs to return HTTP 400 with the code
BAD_FORM_DATAand a message that shows the expected form, when the request body is not a single uploaded file in the expected form field. Before this fix, such a request toPOST /api/v5/plugins/{name}/config/uploadorPOST /api/v5/plugins/installreturned HTTP 500.
Operations and Observability
-
#18886 Rejected invalid Backup Sync interval and timeout values, and suppressed false-positive TLS certificate path errors for successful HTTP synchronizations.
-
#19040 Fixed an issue where importing a namespaced configuration from an older EMQX version (6.1 or earlier) could fail due to missing limiter configuration (
delivery_bytes,delivery_messages,subscribes), which does not exist in such versions. -
#19120 Fixed
emqx ctl conf load --mergeandPUT /api/v5/configs?mode=mergeso that they no longer reset omitted fields to their defaults. Before this fix, loading one field of a config struct reset every other field of that struct. For example, loading onlylisteners.tcp.default.tcp_options.active_nreset the listener'sparse_unit,bind,acceptorsandmax_connections, and loading onlymqtt.max_packet_sizeresetmqtt.idle_timeoutandmqtt.max_inflight. Merge mode now keeps the stored values of omitted fields. A merge no longer needs to repeat the required fields of a struct that the stored configuration already holds, for examplebootstrap_hostsof a connector ortypeof a schema registry entry. Merge mode also keeps the stored authorization sources when the loaded file does not containauthorization.sources. Replace mode is unchanged. -
#19148 The
license_expiryalarm now carries a message that says whether the license is about to expire or has already expired, together with the expiry date. It previously reported the alarm name only, which read as an expired license even when the alarm was the 30-day advance warning. The alarm details gained adays_leftfield, and both the details and the message are kept up to date while the alarm stays active. -
#19163 Fixed
emqx ctl conf load --namespace <ns> --merge: the loaded configuration is now merged over the namespace's stored configuration. Before this fix it was merged over the global configuration, so a namespaced merge could copy global values into the namespace. -
#19166 Fixed an issue where the
emqx ctl clients statscommand stopped with an error and left a partial CSV file when clients connected or disconnected while the command was running. -
#19269 Fixed
emqx ctl clients listandemqx ctl exclusive listso they no longer stop with an error when clients connect, disconnect or unsubscribe while the command runs.emqx ctl clients listskips a client that disconnects before the command prints it. -
#19430 Fixed several
emqx ctlcommands that printed their output and then exited with code 1. A shell script that runs withset -e, or chains commands with&&, stopped after these commands even though they had succeeded. These commands now exit with code 0 on success:broker stats,broker metrics,vm,vm all,vm load,vm memory,vm process,vm io,vm ports,mnesia,topics show,subscriptions show,traces list,gateway-clients list,rebalance startandrebalance stop. The audit log also recorded each successful run of these commands aterrorlevel. It now records them atinfolevel.
emqx ctl rebalance and emqx ctl a2a_registry now exit with code 1 when the command fails.
-
#19441 Fixed
emqx ctlcommands that gave wrong or missing output: -
emqx ctl conf reloadnow prints the reason, the effective configuration and the attempted configuration when it cannot apply a configuration root on the local node. A successful reload now reports the node name instead ofcluster. emqx ctl conf load --namespace <ns> --replacenow shows the configuration stored in that namespace when it cannot apply a configuration root. Before this fix, it showed the global configuration.emqx ctl listeners enablenow printsListener not foundfor a listener that does not exist.-
emqx ctl topics show <Topic>now prints the routes of the topic. Before this fix, it printed nothing and exited with code 1. -
#19616 Fixed a crash loop in the license resource monitor when the license is temporarily not available. Before this fix, the monitor crashed and restarted at each check interval and logged an error each time. Now the monitor continues to run, and the license alarms continue to work when the license is available again.
-
#19618 Fixed a false
failed_to_read_data_filewarning during backup export. The warning was logged for configuration values that only start with the data directory name, for exampledata_sourcein a Tablestore action. The exported backup was always correct. Only the log was wrong. -
#19660 Fixed partial configuration updates through
PUT /api/v5/configs?mode=merge. Such a request failed with aconverter_crashederror when it updated themqroot but omitted the Message Queueauto_createsettings. -
#19679 Fixed an issue where
emqx ctl listenersreported the previous WS or WSS connection limit after a runtime configuration update. -
#19689 Fixed partial configuration updates through
PUT /api/v5/configs?mode=merge. An omitted setting could fail the request or overwrite the stored value: -
The request failed with a
converter_crashederror when it omittedlog.file, for example in a merge of onlylog { console { ... } }. The same happened for any partial merge of thedashboardroot, for theserverof an InfluxDB, Elasticsearch or Datalayers connector, and for thekeyof a message transformation operation. - An omitted TLS
cipherssetting reset the configured cipher list to the default. - A partial merge of the
listenersroot brought back the deleted default listeners.
Durable Storage, Message Queues and Streams
-
#19008 Fixed an issue where Message Queue garbage collection could continuously create durable-storage generations when shard metadata reads failed, causing excessive ETS table and memory usage.
-
#19010 Fixed an issue where Streams garbage collection could continuously create durable-storage generations when shard metadata reads failed, causing excessive ETS table and memory usage.
-
#19211 Fixed an issue where garbage collection failed to delete expired messages from last-value queues and regular queues with message count or byte limits.
-
#19216 Fixed an issue where garbage collection failed to delete expired messages from last-value streams and regular streams with message count or byte limits.
-
#19259 Fixed an issue where a Message Queue could stop delivering new messages from a storage shard after its consumer restarted, for example after a node restart or after all subscribers had been disconnected for a while. This happened when the queue had already read and acknowledged every message in the shard's previous storage generation.
-
#19264 Fixed a Message Queue consumer crash when a subscriber acknowledged a message from a stream that the consumer was still restoring after losing its durable storage subscription.
-
#19304 Fixed several durable-session cleanup and expiration issues:
-
Prevented a new connection from reading stale session data during session takeover.
- Made session deletion one-way to prevent half-closed sessions from being restored and reused.
- Released RocksDB column-family handles after dropping slabs so disk space can be reclaimed without restarting the node.
- Added retries for durable-session expiration timers.
- Guarded expiration timers with the channel cookie so an old timer cannot delete a session after takeover, including during a network partition.
-
Corrected the
awaiting_rel_cntcounter for disconnected durable sessions. -
#19551 Fixed a race during Raft leadership changes that could crash a durable storage shard when a delayed transaction startup notification arrived while the transaction process was stopping.
-
#19572 Fixed a problem where a durable session could hang upon failing to commit its state to the database.
-
#19621 Improved the robustness of durable sessions when the
sessionsdatabase is overloaded. EMQX now treats commit timeouts as recoverable and adds jitter to checkpoint timers to reduce simultaneous commit spikes. -
#19652 Fixed an issue where a recoverable failure during an asynchronous state checkpoint could leave a durable session in an inconsistent state.
-
#19668 Fixed rare cases where a client that consumes a stream or retained messages stopped receiving messages, or was disconnected because its connection process crashed. This could happen after a session takeover, after the session dropped messages (for example because of No Local or message expiry), or when the delivery rate limit was reached.
When the delivery rate limit is reached, messages from streams and message queues now wait at their source instead of in the session message queue. QoS 0 messages that the delivery rate limit drops now count in the delivery.dropped metric and trigger the delivery.dropped event.
- #19753 Fixed
DELETE /api/v5/clients/{clientid}so that it deletes an offline durable session. Previously, the API returned HTTP 204 without deleting the session.
Breaking Changes
- #18788 Disabled automatic creation by default for
mq.auto_createandstreams.auto_createas a security enhancement.
Previously, subscribing to a $queue/... topic created a last-value message queue, and subscribing
to a $stream/... topic created a last-value message stream, when the queue or stream did not exist.
Both now default to false, so a subscription no longer creates anything implicitly.
To keep the previous behaviour, enable automatic creation explicitly:
mq.auto_create.lastvalue = {}
streams.auto_create.lastvalue = {}
Set regular instead of lastvalue to automatically create regular queues or streams. Only one of
the two can be enabled at a time. Message queues and streams that already exist are not affected.
-
#18974 Added
mqtt.max_connect_user_properties, which limits the number of MQTT v5 User Property pairs accepted separately in CONNECT properties and Will properties. The default is 100; set it toinfinityto disable the limit. -
#19774 For in-memory sessions, renamed the internal message header
deliver_begin_attodeliver_begin_at_usand changed its timestamp unit from milliseconds to microseconds.
Plugins and hook handlers that read this header must use the new name and adapt to the new precision.
- #19910 Fixed an issue where the RabbitMQ connector's
heartbeatvalue was converted to milliseconds before being passed to the RabbitMQ client, which interprets the value as seconds. When the remote AMQP server proposed a heartbeat of0, idle connections could time out and reconnect repeatedly. EMQX now passes the configured value in seconds.