Download Latest Version EMQX Enterprise (LTS) 6.3.2 source code.zip (16.0 MB) Google Add to Preferred Sources
Home / 6.0.4
Name Modified Size InfoDownloads / Week
Parent folder
EMQX Enterprise 6.0.4 source code.tar.gz 2026-09-29 6.4 MB
EMQX Enterprise 6.0.4 source code.zip 2026-09-29 8.3 MB
README.md 2026-09-29 91.2 kB
Totals: 3 Items   14.8 MB 0

6.0.4

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
macos14 arm64 .zip (sha256)
macos15 arm64 .zip (sha256)
macos26 arm64 .zip (sha256)

Plugins

Plugin Version Package
emqx_backup_sync 0.1.1 .tar.gz (www.emqx.com) (sha256)
emqx_relup 1.0.2 .tar.gz (www.emqx.com) (sha256)
emqx_sync_request 0.1.0 .tar.gz (www.emqx.com) (sha256)
emqx_username_quota 1.2.1 .tar.gz (www.emqx.com) (sha256)

Breaking Changes

  • #18515 Azure Blob Storage Action's blob template field now has the same schema validation as Aggregated S3's key, which verifies the allowed bindings are followed.

  • #18528 Now, the exporter endpoint of an Opentelemetry integration is validated to be an URL with scheme and port. Supported schemes are http and https, and the port must be explicitly set.

  • #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 to infinity to disable the limit.

Enhancements

Core MQTT Functionalities

  • #19096 Added the mqtt.max_connect_packet_size setting. It limits the size of a CONNECT packet, in addition to mqtt.max_packet_size. EMQX closes the connection of a client whose CONNECT packet is larger, and counts it under the listener shutdown counter connect_packet_too_large. The default is 1MB, the same as the default mqtt.max_packet_size, so nothing changes until the setting is lowered.

  • #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.

Access Control

  • #17732 Added a namespace field to the API key creation and update endpoints, so operators no longer need to encode the namespace inside the role string (the existing ns:<namespace>::<role> form keeps working). When both forms are supplied they must agree.

  • #17813 The Dashboard user and API-key endpoints now reject scope lists that mix privilege scopes (system, user_management, api_key_management, sso_management) with other scopes. Each of the four privilege scopes is administrator-equivalent in effect, so combining them with a restricted scope list cannot meaningfully restrict the account. Use either a privilege-only scope list or a non-privilege-only scope list, depending on whether the account should have administrator-equivalent capability. Pre-existing records with a mixed scope set continue to function until the next update; the next update must split the list to succeed.

  • #17855 Namespace-scoped dashboard administrators can now create, list, read, update, and delete API keys within their own namespace. They cannot create global API keys or keys in another namespace, and API keys outside their namespace are hidden from them.

Data Integration

  • #17933 RabbitMQ connector supports a multi-node servers list (e.g. rmq1:5672,rmq2:5672) with connect-time failover and rotated pool start offsets. Legacy server/port remain when servers is unset.

  • #17944 Added OAuth2 Client Credentials authentication to the HTTP connector and HTTP authentication/authorization. When enabled, EMQX obtains and refreshes an access token from the configured token endpoint and adds it to outbound requests as a Bearer authorization header.

The connector health check reports disconnected when a token cannot be obtained. Configurations that enable OAuth2 and also provide an Authorization header are rejected.

EMQX sends the client ID and client secret as form fields in the token request body. Sending the credentials in the HTTP Basic Authorization header is not supported.

  • #18014 Datalayers Arrow Flight connector now enables automatic rebuild of prepared statements. When the server loses a prepared statement (e.g., after restart), the client will automatically recreate it and retry the write operation, avoiding write failures.

  • #18042 Added AWS IAM role credential support to DynamoDB 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.

  • #18081 Improved resiliency of Snowflake Streaming Action. Under certain error types when appending rows (specifically, when the channel internal states becomes out of sync), the Action will retry the rows that failed and attempt to re-open the channel without manual intervention.

  • #18085 Added new configuration options for the Kafka, Confluent, and Azure Event Hubs producers:

  • max_batch_age (action): drop messages that stay in the producer buffer longer than this duration instead of sending them; counted in the dropped.expired metric. Default: infinity (never drop).

  • max_retries (action): drop a message batch after this many failed retries; counted in the failed metric. The retry counter is incremented only when Kafka explicitly responds with an error code; resends after a connection loss do not increment it. Default: infinity (retry forever).
  • reconnect_delay (action): delay before the producer reconnects after a connection loss. Default: 2s (previously hard-coded).
  • request_timeout (connector): how long to wait for a reply from Kafka before the connection is considered stale and gets re-established. Default: 30s.

Additionally, the Kafka client library upgrade (wolff 4.2.1) restores max_linger_time support for memory-mode buffers: an under-sized batch now waits up to max_linger_time for more messages, reducing the produce request rate; full batches are sent without delay.

  • #18110 Added support for JSON Schema drafts 2019-09 and 2020-12 in Schema Registry.

  • #18137 The GCP Pub/Sub producer and consumer now accept a fully-qualified topic path (projects/<project-id>/topics/<topic-name>) in the topic configuration, making it possible to publish to or consume from a topic that lives in a different GCP project than the service account's own. A bare topic name keeps resolving against the service account's project as before. For consumers, the subscription is still created in the service account's project; only the topic reference may point to another project.

  • #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]:9092 or [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_family option selects the IP address family. With the default auto, a hostname is tried over IPv4 first and then over IPv6. Set it to ipv6 to connect over IPv6 only, or to ipv4 to 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.

  • #19313 Improved the detection of unresponsive connections in HTTP-based connectors, such as HTTP Server, GCP PubSub Producer, Couchbase and Snowflake. Before this fix, a connection with requests waiting for a response was reconnected only after the full request timeout (resource_opts.request_ttl) plus max_inactive had passed. With a large request_ttl this took a long time. With request_ttl = infinity, the connection was never reconnected.

Now a connection is reconnected when no request has been sent on it for 60 seconds while requests still wait for a response, or for max_inactive when that is longer. The default settings (request_ttl 45 seconds, max_inactive 10 seconds) are not affected.

This changes the behavior of connectors configured with a request_ttl above about 50 seconds. A response that takes longer than 60 seconds can now be interrupted by a reconnect. If the remote service is expected to take longer than 60 seconds to respond, set max_inactive to at least the slowest expected response time.

Plugins

  • #18012 Added the emqx_sync_request plugin for synchronous MQTT request/response flows through the EMQX REST API. It also provides node-local CLI diagnostics for request counters and current pending state.

Observability

  • #17712 Added emqx_session_tool, an operator-facing diagnostic module callable from the remote console. Use emqx_session_tool:top_by(mqueue_len) (or any other session metric such as mqueue_dropped or inflight_cnt) to find the top-K sessions by gauge or counter value in clusters with many connections, without paging through the client list manually. The scan streams the channel registry, keeps only a bounded top-K result, and reads cached per-session metrics without messaging connection processes. emqx_session_tool:cluster_top_by/1 aggregates the result across all cluster nodes.

Performance

  • #18185 Improved deep-page queries in the subscriptions HTTP API by accumulating in-memory subscription rows on each target node, avoiding one RPC per pagination batch.

  • #18229 Reduced CPU overhead on the data-integration send path. The broker no longer builds a formatted error string for every message routed through a resource that is not an action or source (for example, cluster-link message forwarding), which could previously trigger long-scheduler warnings under high message volume.

  • #19240 Reduced temporary memory allocation when receiving MQTT PUBLISH messages, without changing packet validation.

Deployment

  • #18037 Added Enterprise Linux 10 (EL10) packages, for Red Hat Enterprise Linux 10, Rocky Linux 10, and compatible distributions.

  • #18127 Start releasing macOS 26 (Tahoe) packages.

Bug Fixes

Security

  • #16389 Fixed an issue where the OAuth2 client_secret was 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.

  • #17644 Fixed an issue where the plain password hash algorithm accepted passwords that differed only by letter case during authentication.

  • #17653 Fixed a security issue where the Prometheus configuration API returned stored Authorization header values in push gateway headers. The API now redacts these values in responses.

  • #17654 Fixed an issue where creating an authenticator via POST /authentication returned the new authenticator config without redacting provider secrets (such as JWT HMAC secrets, HTTP Authorization headers, and request body passwords). The creation response now applies the same redaction as the list and get endpoints.

  • #17657 Fixed a security issue where raw authorization and cookie headers were forwarded to plugin API callbacks. These credential-bearing headers are now redacted before reaching plugin code.

  • #17739 Improved redaction of sensitive data in logs, traces, and audit records.

  • #17765 Fixed missing authorization checks in several gateway broker paths.

MQTT-SN will message publishing, JT808 upstream publishing and automatic downlink subscription, GBT32960 upstream publishing and automatic downlink subscription, and OCPP upstream publishing and automatic downlink subscription now check authorization before publishing or subscribing.

  • #17787 Stopped HTTP connector error logs from including request headers when an ehttpc worker is killed mid-request.

When the HTTP connector's ehttpc worker was killed while a request was in flight (for example, by deleting the source while the request had not yet returned), the resulting EXIT reason carried the original gen_server:call arguments, which include the request headers. These headers were then written verbatim to the error log. The call arguments are now dropped from the reason before it is logged.

  • #17790 Stopped writing the TOTP shared secret to the dashboard_login_failed server log. The secret was previously included in this log entry during first-time MFA setup.

  • #17791 Improved log redaction so that JWT HMAC key bytes no longer appear in cluster_rpc_apply_result and cluster_rpc_apply_ok debug log lines emitted during configuration updates.

The redactor now recognises the internal JWK record shape and replaces it with a placeholder before logging, and also treats the jwk field as sensitive.

  • #17853 Improved redaction of sensitive HTTP request headers in connector debug logs. x-api-key, x-auth-token, api-key, and cookie headers are now stored as secrets in connector state (matching the existing behaviour for Authorization / Proxy-Authorization), so their values are not printed when connector state is emitted at trace / debug level. In addition, the shared header-redaction helper now recognises header names that are stored as iolists (a shape produced by the connector's template parser), which previously slipped through the sensitivity check.

  • #17888 Fixed an issue where the LwM2M gateway could include sensitive REGISTER query fields such as password, secret, private_key, and access_token in registration/update MQTT reports.

  • #17974 Raw MQTT packet data is now redacted by default in connection logs; trusted client IP addresses can be allowlisted per listener for diagnostics.

  • #18005 Fixed an issue where CLI audit logs could store sensitive command arguments.

  • #18051 Fix CoAP debug logs leaking sensitive URI-query values.

  • #18146 Hardened scope-based authorization for the dashboard and management API so that access-control checks are applied consistently across equivalent request paths.

  • #18293 Upgrade QUIC stack to quicer-0.4.8 (msquic 2.5.7).

Contains a security update for CVE-2026-32179

  • #18302 Elasticsearch action index and id values are now URL-encoded when composing the request path, so characters such as # or / in a templated value are treated as literal text within a single path segment instead of altering the request target. The JSON request body is not affected.

  • #18314 When reading GCP Connectors (GCP PubSub Producer/Consumer, Bigquery) that use JSON Service Account authentication via the HTTP API, now the values are redacted.

  • #18330 Read-only REST endpoints no longer return secrets in cleartext:

  • GET /listeners and GET /listeners/{id} now render the listener ssl_options.password as ******.

  • GET /exhooks and GET /exhooks/{name} now render the gRPC client ssl.password as ******.
  • Audit log entries for POST /license now record the request body as ******, so the license key does not appear in GET /audit results.

Updating a listener or an exhook server with a body that contains the ****** placeholder keeps the stored secret unchanged.

  • #18344 Upgraded HOCON to 0.46.3. This release renders sensitive values inside array-typed config fields as ****** and no longer prints sensitive field values in config validation error logs.

  • #18391 Fixed an authentication cache key collision. Two different credentials whose fields concatenate to the same bytes could share a cache entry, letting one client receive another client's cached authentication result within the cache TTL.

  • #18580 Redact sensitive configuration values in the conf.hocon file produced by the bin/node_dump script.

Values marked as sensitive in the configuration schema, such as dashboard.default_password and license.key, are now written as ******. Before this fix, the script redacted only a fixed list of key names, so these values were written in plain text.

  • #18708 Avoid logging sensitive information in debug mode.

Running bin/emqx commands with DEBUG=1 or DEBUG=2 no longer prints the Erlang cookie or the license key in the shell trace output.

  • #18836 Stopped including the result of a successful configuration change in the cluster configuration sync debug logs.

The result could carry compiled runtime state, such as the HTTP authenticator header templates, which held secrets that log redaction did not cover.

  • #18853 Fixed an issue where SSO MFA setup and verification credentials could be stored unredacted in audit logs when audit logging was enabled.

SSO MFA request bodies now keep only username and backend in audit records; temporary tokens, TOTP codes, and unknown credential fields are redacted before being written to audit files or the audit database.

  • #18855 Fixed MQTT-SN session wake-up authorization for DTLS clients authenticated with verified client certificates. A certificate-bound session can be resumed from a new association only when it presents the same peer certificate. Wake-up without a client certificate or with a different or reissued certificate is rejected.

Plaintext UDP and DTLS clients without a certificate retain the legacy ClientId-only PINGREQ wake-up behavior.

  • #18959 MQTT listeners now close a connection as soon as its first packet is not a CONNECT, before reading the packet body.

  • #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.

  • #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.password and the gateway authentication configuration. These values are no longer logged.

  • #19037 Gateway debug logs no longer contain raw datagrams: received_data, received_udp_proxy_data and send_data now 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/:name and GET /api/v5/opentelemetry by masking credential-bearing values in the returned configuration. The corresponding create and update endpoints accept the masked values back without overwriting the stored ones.

  • #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.

Core MQTT Functionalities

  • #18111 When mqtt.strict_mode is enabled, MQTT v3.1 CONNECT packets that set the password flag without the username flag are now rejected, matching the existing behavior for MQTT v3.1.1. The MQTT v3.1 specification states that it is not valid to supply a password without a user name.

Additionally improved connection log readability: the CONNECT packet trace now prints Password=undefined when no password was supplied (previously indistinguishable from an empty password), and the peername field in logs is now always rendered as a plain string such as 10.0.0.1:54123.

  • #18181 Fixed an issue where rate limiters configured with a burst value of 0 could still allow an extra burst of traffic. This made limits such as MQTT delivery message rate limits less strict than configured.

  • #18236 Fixed an issue where clients using socket-backed TCP listeners could be unexpectedly disconnected under high load, due to occasional readiness signals arriving for not-yet-ready sockets.

[error] crasher: initial call: emqx_socket_connection:init/4, ..., error: {{case_clause,{select,{select_info,recv,#Ref<...>}}},[{emqx_socket_connection,handle_msg,2,[{file,"emqx_socket_connection.erl"},{line,827}]}, ...

  • #18357 #18375 MQTT connections are now refused until node startup completes, so listeners no longer serve traffic before authentication, authorization, and plugin hooks are active.

The GET /status API now returns HTTP 503 until startup completes, so load balancers can route new connections to other nodes in the cluster.

A cluster join request toward a node that has not finished starting is now refused with a message that asks to retry later.

  • #18523 Stop MQTT listeners before stopping applications during node shutdown.

Previously, listeners kept accepting and processing client traffic while the applications behind the publish path were already stopped. Publishing clients could then trigger a burst of hook_callback_exception errors in the log, for example from the rule engine, until the listeners stopped a few seconds later. Listeners now stop first, so no client traffic is processed during application shutdown.

The node now also reports itself as not running in GET /status as soon as shutdown begins, so load balancers stop routing new connections to it.

  • #18585 End a session that does not outlive its connection when a new connection takes over the same client ID, as the MQTT specification requires. This covers MQTT 5.0 clients connecting with Session Expiry Interval 0 and MQTT 3.1.1 clients connecting with Clean Session 1.

Before this fix, the new connection could inherit the old session's subscriptions and queued messages, and a will message with a Will Delay Interval greater than zero was silently dropped. Now the new connection starts a fresh session (CONNACK Session Present 0), the old connection receives DISCONNECT with reason code 0x8E (Session taken over), and its will message, if any, is published at the takeover.

  • #18789 MQTT and Gateway listener creation now rejects listener names longer than 64 bytes or not matching the restricted-name format with a clear BAD_REQUEST response. Existing listeners with longer names remain editable.

Durable Storage and Message Queue

  • #17733 Fixed an issue where MQ consumers could fail to restore an empty stream buffer after durable storage subscription recovery.

  • #18143 Fixed an issue where durable shared subscriptions could fail to communicate with the shared-subscription leader when subscribers were connected to a different node. This could cause an unexplained spike in CPU usage.

  • #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.

Rule Engine

  • #17725 Fixed a bug introduced in 6.0.3, 6.1.2 and 6.2.1 where a global rule could stop matching messages on its FROM topic when the publishing clients carry a tenant namespace (client_attrs.tns).

With rule_engine.limit_selects_in_namespace enabled (the default), global rules now retain system-wide visibility and match messages from any namespace. Rules created inside a namespace remain isolated to their own namespace.

Operators who prefer to disable namespace restriction entirely can still set rule_engine.limit_selects_in_namespace = false.

  • #17957 Fixed an issue where multiple rule events (for example, $events/client/connack) would not trigger rules in the global namespace when rule_engine.limit_selects_in_namespace = true.

  • #18049 Fixed an issue where setting rule_engine.limit_selects_in_namespace = true would prevent alarm activated/deactivated-triggered global rules from firing.

  • #18110 Fixed an issue where using the examples annotation in a draft-06 JSON Schema in Schema Registry would result in valid data being rejected as invalid.

  • #18198 Fixed two JSON Schema Registry issues:

  • Schemas containing non-ASCII characters (for example Chinese property names or example values) can now be registered through the HTTP API. Previously, registration failed with an internal badarg error.

  • $ref references pointing at definition names containing non-ASCII characters now resolve correctly during validation and decoding, both in percent-encoded form (for example #/definitions/%E5%A7%93%E5%90%8D%E7%B1%BB%E5%9E%8B) and in raw UTF-8 form. Previously, such references failed to resolve, and decoding failed with an internal badmatch error.

In addition, a payload that does not conform to its JSON schema now produces a clear schema validation error during Rule Engine decoding instead of an internal error.

  • #18303 Sparkplug B alias-to-name mappings are now maintained only for messages published directly by MQTT clients. Messages arriving through bridges or other internal paths no longer share alias mappings, which prevents one publisher's mapping from being applied to another publisher's decoded metrics. As a consequence, spb_decode no longer resolves aliases to metric names for data messages ingested through an MQTT bridge.

  • #18527 Fixed repeated badarg errors in the log when a message was published while the schema validation, message transformation, or rule engine topic index table was unavailable. Such a publish now proceeds as if no validation, transformation, or rule matched the topic, and the broker logs a throttled topic_index_table_missing message instead of one error per publish. The index tables now also survive a restart of their owner process, and the hooks are removed before the tables during application shutdown, which removes the known windows where a publish could find a table missing.

  • #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.

Data Integration

  • #17598 Fixed a connection failure to MongoDB 8.0+ when authentication is required. The driver previously queried buildInfo before authentication to pick the auth mechanism; MongoDB 8.0 restricted that command to authenticated callers. The driver now skips the probe and uses SCRAM-SHA-1 directly, which all supported MongoDB versions accept.

  • #17605 Fixed Oracle action prepare/status checks to parse action SQL without executing it, and reject unsupported top-level DDL/DCL/TCL statements. Also improved support for text payloads over 4000 bytes when the payload placeholder is the last bind parameter.

  • #17625 Fixed an issue with GCP PubSub Consumer Source where, if a source was initially created with a service account lacking necessary permissions to create subscriptions for the configured topic, the Source would fail to become connected even after granting the permissions to the service account.

  • #17649 Improved the responsivity of starting/stopping GCP PubSub Consumer Connectors. Previously, if hte connections were slow/busy, there could be timeouts which left the Connectors running, in an inconsistent state with the config.

  • #17681 Fixed PostgreSQL connector batch writes when prepared statements are disabled.

Previously, concurrent batches on the same connection could interleave raw SQL parsing and fail with PostgreSQL protocol errors. Table-existence checks are also serialized through the connector worker to avoid interleaving with batch execution.

  • #17717 Added the option of allowing TLS Peer Verification for Confluent Producer Connectors.

  • #17718 Added the option of allowing TLS Peer Verification for GCP PubSub Producer/Consumer and BigQuery Connectors.

  • #17859 Fixed the MQTT connector so it can connect to IPv6 brokers.

Previously, configuring an MQTT connector to an IPv6 broker failed in two ways: an IPv6 literal such as [::1]:1883 was rejected at save time with a bad_host_port validation error, and a hostname that only resolves to an IPv6 (AAAA) address failed to connect with a "Could not resolve host" error because the connection defaulted to IPv4.

The server address parser now accepts bracketed IPv6 literals (for example [::1], [::1]:1883, and mqtt://[::1]:1883), and the MQTT connector now enables IPv6 probing when connecting, so IPv6-only brokers can be reached.

The MQTT connector and cluster link server address now accept the official MQTT URI schemes mqtt (plain TCP) and mqtts (TLS), for example mqtt://broker:1883 and mqtts://broker:8883. A scheme-less host:port is still accepted. Any other scheme is now rejected with an unsupported_scheme validation error.

  • #17947 Fixed an issue where updating an HTTP connector could leave its action buffer workers blocked after the connector was recreated, causing messages to remain queued until the next retry interval.

  • #17955 Fix GreptimeDB async batches that could remain unflushed after health checks at low write rates.

  • #17961 Fixed an issue where a Kafka or Pulsar Connector would transition to a disconnected state on health check timeouts, potentially recreating its internal queue. Now, they transition to connecting.

  • #17970 When SSRF protection is enabled, managing connectors is no longer disrupted by an existing connector whose address is now blocked by the policy.

Previously, enabling SSRF protection (or extending its deny list) after connectors were created could make unrelated connector operations fail with an internal error, and deleting an affected connector could leave it behind after its actions and rules were already removed.

SSRF protection now applies to HTTP and MQTT connectors and is enforced when a connector is created or updated: creating or updating such a connector with a blocked address is rejected. Enabling, disabling and deleting connectors are never blocked, and other connector types are not subject to the policy.

  • #17973 Fixed Kafka producer action retry metrics. The retried, retried.success, and retried.failed counters on an action's metrics now reflect messages that the internal buffer re-sends after a broker reconnect, so an operator can tell whether retried messages ultimately succeeded or failed. Previously these counters stayed at 0 regardless of how many internal retries occurred. The success and failed counters are unaffected and are not double-counted.

  • #17982 GCP PubSub Consumer now uses HTTP2 and cancels its pull request when it reaches the timeout. This signals more cleanly to the GCP server that it may lease the messages to a new pull request, reducing tail latencies.

  • #18055 Fixed an issue where Snowflake Streaming Actions on different nodes in a cluster would start to fail with the following error:

{unrecoverable_error,#{body => <<"{\"code\":\"STALE_CONTINUATION_TOKEN_SEQUENCER\",\"message\":\"Channel sequencer in the continuation token is stale. Please reopen the channel\"}">>,...

  • #18174 The MQTT connector now reports a clear error message when the server address scheme is inconsistent with the SSL settings, for example an mqtts:// (TLS) address while SSL is disabled.

Previously, such a configuration failed with an internal error and a noisy log, because the connector attempted a plain TCP connection to a TLS port and could not interpret the server's reply. Connection attempts that receive non-MQTT data from the server (for example, when the port expects TLS) now also produce a clear explanation instead of an internal error.

  • #18270 Fix GreptimeDB connectors that could fail to restart when a stale gRPC channel remained after a worker was force-stopped.

  • #18274 Fixed the Tablestore connector health check listing all timeseries tables on every check. Health checks now use a DescribeTimeseriesTable probe against the configured probe_table_name, falling back to listing all timeseries tables when it is unset.

  • #18299 Fixed an issue where the Snowflake connector's configured TLS (ssl) settings were not applied to its outbound HTTPS connections (both Streaming and Aggregated modes). Settings such as verify, cacertfile, client certificates, and server_name_indication were accepted and displayed but had no effect on the actual connections. The configured values are now honoured. Connectors that never customized the ssl settings keep the previous connection behavior.

  • #18392 Fixed an issue where aggregated Actions (S3, S3Tables, Azure Blob Storage, Snowflake Aggregated) with the same name but in different namespaces would share the same working directory for their temporary files.

  • #18449 On rare race conditions, the Postgres Action could get a sock_closed error while writing data, and would treat that as an unrecoverable error. Now, it's treated as a recoverable error.

  • #18465 Fixed handling of templated INSERT SQL statements in the ClickHouse, TDengine, SQL Server, and MySQL bridges (when batch insert is enabled).

Previously, rendering SQL templates could often produce malformed SQL due to syntax errors in the manually entered template itself and due to interpolation issues.

Now, SQL statements are fully parsed when an action is created, and invalid SQL is rejected. During rendering, correct escaping is enforced. To provide consistent and predictable behavior, we limit the SQL features that can be used. Most notably, we reject comments in SQL statements. However, we support a large subset of syntax features: constant values, strings and string interpolation, arithmetic, functions, conditions, and conditional operators.

MySQL also supports ON DUPLICATE KEY UPDATE, ClickHouse supports FORMAT Values and FORMAT JSONCompactEachRow, and TDengine supports INSERT ... USING ... TAGS and table identifier interpolation.

To provide consistent rendering for MySQL templates, the MySQL bridge unconditionally disables ANSI_QUOTES and NO_BACKSLASH_ESCAPES modes for all connections, and treats the statements accordingly.

The ClickHouse bridge now infers the batch value separator from the SQL template and ignores the configured batch_value_separator value.

  • #18763 Fixed an error reported by the TDengine action. When the action could not be found, the error named the connector's ID instead of the action's ID, which made the error read as if a valid connector ID was invalid.

  • #18846 Fixed SQL template rendering in data integrations. Doris batch inserts now use Doris-compatible syntax and escaping for text and binary values. MySQL templates now handle escaped dollar signs correctly.

  • #18859 Fixed an issue where changing only the letter casing of a sensitive HTTP header name while updating a configuration could remove its stored value.

  • #18941 Fixed GreptimeDB connectors that switched between connected and disconnected under heavy write load. The health check no longer waits behind pending writes, so it fails only when GreptimeDB does not respond.

  • #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 disconnected with 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_found error, and the log no longer contains the message payload.
  • With a namespace configured, the auto-create default topic TBW102 was 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

  • #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.

Clustering

  • #17995 Fixed an issue that could terminate a node while it joined a cluster whose persisted mqtt.max_packet_size differed from its local configuration. EMQX now skips listener refresh side effects before listener startup and creates the listeners from the synchronized configuration when the EMQX application starts.

  • #17999 Fixed a startup crash-loop that could occur when a node using the community (single-node) license joins a cluster whose peers hold a clustering-capable license.

Previously, if cluster membership was established before the peer's license was replicated to the joining node, the node would refuse to start with a SINGLE_NODE_LICENSE error and, under an automatic-restart supervisor, keep crash-looping. The node now waits a bounded grace period for the clustering license to sync before it starts. A cluster in which no node ever obtains a clustering license is still rejected after the grace period elapses.

  • #18077 Fixed a crash when a node received a cluster join request (CLI or API) before it had fully booted: joining restarts the internal database while applications are still starting, which could bring the whole node down. Such requests are now rejected with a clear error message; retry after the node is fully started.

  • #18347 Fix a problem with Mnesia RocksDB backend that caused table inconsistency on core nodes when keys were deleted while core node is down.

From the EMQX point of view, this problem could lead to delayed release of dashboard login locks, as well as wasted disk space by the EMQX schema registry, since deletion of old schemas could be missed.

  • #18537 Fixed Cluster Linking to classify temporary message-forwarding connection errors as recoverable. Messages affected by transient network outages are now buffered and retried instead of being counted as failed.

  • #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.

  • #19144 Fixed an issue where the session registry cleanup process stopped when a client disconnected while the cleanup was running. The cleanup now continues past the disconnected client and keeps removing stale registrations.

  • #19160 Fix the following crash in mria application that occurs when a core node joins or leaves the cluster:

Reason: {badarg,[{ets,select,[gproc,[{{{{n,l,{mria_rlog_replica,route_shard_m,'$1'}},n},'$2','_'},[],[{{'$1','$2'}}]}]],[{error_info,#{cause => id...

This fix removes gproc dependency of mria application from the list of business-level applications that reboot when the node changes the cluster.

Configuration Management

  • #17773 Fixed configuration update commands (REST API and CLI) crashing with a function_clause crash report when the underlying cluster RPC layer aborted with an unexpected reason, for example {no_exists, cluster_rpc_mfa} when the cluster RPC tables were not yet available during node startup or recovery. Such failures are now returned to the caller as a structured error instead.

  • #18277 Improved reliability of persisting configuration changes to cluster.hocon: the update is now written and synced to disk before atomically replacing the file, and a failure to read the previous file for backup no longer prevents the new configuration from being saved.

  • #18383 Fixed an issue where submitting a configuration containing an invalid Unicode escape sequence through PUT /configs returned an internal error. Such requests now return a validation error that names the invalid escape.

  • #18444 Fixed the byte-size units b and B requiring quotes in configuration files.

max_packet_size = 1MB was accepted, but max_packet_size = 1B failed to parse and had to be written as "1B". All byte-size units are now accepted without quotes.

  • #18464 Fixed a rare crash in the ExHook manager when an ExHook server became unhealthy during a configuration update. The manager now keeps the configured server order and continues serving configuration changes while the server reconnects.

  • #19120 Fixed emqx ctl conf load --merge and PUT /api/v5/configs?mode=merge so 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 only listeners.tcp.default.tcp_options.active_n reset the listener's parse_unit, bind, acceptors and max_connections, and loading only mqtt.max_packet_size reset mqtt.idle_timeout and mqtt.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 example bootstrap_hosts of a connector or type of a schema registry entry. Merge mode also keeps the stored authorization sources when the loaded file does not contain authorization.sources. Replace mode is unchanged.

  • #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.

Access Control

  • #17646 Fixed an HTTP/1.1 protocol-conformance issue in the JWKS retrieval client used by JWT authentication. Earlier versions sent an empty TE: header value due to a long-standing default in Erlang/OTP's inets HTTP client (fixed upstream in inets 9.4.2 / OTP 28.1). Some identity providers (notably PingFederate) reject such requests. EMQX now sends an explicit, valid TE: trailers header on JWKS fetches.

  • #17975 The /tracing configuration endpoint (PUT /api/v5/tracing) is now restricted to the global administrator. Namespaced dashboard administrators and API keys can no longer mutate the global [trace] configuration; such requests are rejected with HTTP 403.

  • #18009 Fixed an error when editing only the note (description) of the default administrator via the Dashboard user API.

The user API now accepts a scope list that matches the role's implicit full set (and the unset value) as equivalent to "no explicit scopes", so a read-modify-write of an administrator no longer fails. Such users also keep their forward-compatible implicit scopes instead of a frozen list.

  • #18196 Fixed an error when updating an API key that was created with the scope left blank.

The API key create and update requests now accept a scope list that matches the role's implicit default (and the unset value) as equivalent to "no explicit scopes", so re-submitting the value returned by a read no longer fails. Such keys also keep their forward-compatible implicit scopes instead of a frozen list, consistent with Dashboard users.

  • #18221 Fixed the default administrator user being created with an explicit scope list at startup.

The default administrator now follows its role's implicit default scopes (shown as unset), so it automatically gains scopes introduced in future releases instead of being pinned to a frozen list. Existing default administrator records that carry an explicit list are updated to the implicit form at boot.

  • #18222 Namespaced administrator API keys now get the same default scope list as namespaced dashboard users. In particular, the default no longer includes the publish scope, which was misleading: the publish API is global-only and cannot be used by namespaced API keys. Creating a new namespaced API key with an explicit scope list containing publish, or including publish when changing an existing namespaced API key's scope list, now returns a validation error. Existing API keys are unaffected: previously stored scope lists (including ones containing publish) are kept as-is and continue to work exactly as before.

  • #18225 Improved the warning logged when an API key bootstrap file entry contains scopes that are dropped during loading. The warning now groups the dropped scope names by the reason they were dropped -- an unknown scope name, a scope not allowed for the publisher role, or a privilege scope that cannot be combined with other scopes -- instead of reporting every dropped scope as an unknown scope name.

  • #18315 MQTT File Transfer file listing and download REST endpoints are now available only to global (non-namespaced) Dashboard users and API keys. Namespaced users and API keys can no longer read files uploaded by clients outside their namespace.

  • #18576 The OIDC SSO configuration API (GET /api/v5/sso/oidc) now returns client_jwks as none when no client JWKS is configured, matching the CLI output. Previously the value was masked as ****** even when nothing was configured. A configured client JWKS remains masked.

  • #18630 Namespaced administrator API keys can no longer be created, updated, or bootstrapped with scopes the namespaced role is not allowed to hold (such as gateways or audit), matching the existing dashboard user rule.

Rotate any existing namespaced API key that was granted such scopes, since keys already minted with them keep working until rotated.

  • #18681 Fixed REST API authentication error messages to match what the target endpoint accepts. A rejected bearer token or missing authorization header on an endpoint that rejects API keys, such as POST /api/v5/logout, no longer suggests using one. The message also no longer names the api_key.bootstrap_file configuration key.

  • #18724 Dashboard SSO OIDC now rejects configuration where the issuer's URL scheme and the TLS option disagree (https with TLS disabled, or http with TLS enabled) at config time, instead of failing repeatedly at runtime with an unclear TLS error or silently ignoring the TLS option.

  • #18737 Fixed ${peerport} rendering as an empty string in authentication and authorization request templates.

Also added ${peername} to the client information available in these templates. It renders as the client's address and port, for example 192.168.0.1:51544.

  • #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.

  • #18904 Fixed an issue where creating or updating an enabled Dashboard SSO backend could fail with an internal error when its resource remained in the connecting state until the start timeout.

The backend now remains tracked with its resource ID so that it can be cleaned up after the start timeout.

  • #18963 Fixed POST /api/v5/api_key returning HTTP 500 when the optional desc or enable field is omitted from the request body. The key is now created with an empty note and enabled by default. Request body fields that are not part of the API key schema (for example description instead of desc) are still ignored by request validation.

  • #18964 Fixed the SCRAM HTTP authentication backend rejecting the OAuth2 configuration that the Dashboard submits for it. Creating an SCRAM + HTTP Server authenticator with OAuth2 enabled now succeeds, and the access token is sent as a Bearer authorization header on the user lookup request.

  • #19003 Hardened PUT /api/v5/api_key/:name so 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.

  • #19115 Hardened the plugin configuration read endpoints (GET /api/v5/plugins/:name/config and GET /api/v5/plugins/:name/config/download) by limiting them to the global administrator.

  • #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, publish and audit. 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 given gateways, publish, audit or mfa_management.
  • At startup, EMQX removes gateways, publish, audit and mfa_management from the stored scope lists of existing namespaced viewers. Such a viewer loses read access to the gateway endpoints. The publish and audit endpoints were already denied to namespaced users. Stored scope lists of existing API keys are not changed.

Data Backup

  • #17806 Aligned the data backup import and export endpoints with the principle of least privilege: Dashboard users whose scope set does not include both user_management and api_key_management can no longer import or export archives containing the dashboard_users or api_keys table sets. Global administrators and API-key callers with the necessary scopes are unaffected.

  • #17807 Namespaced administrators now have an isolated data backup space. Their exports, uploads, listings, downloads, imports and deletes through the data backup endpoints (/data/export, /data/import, /data/files, /data/files/:filename) only ever act on their own namespace's backups. A namespaced administrator can no longer see, download, or delete global backups or another namespace's backups.

Global administrators continue to manage global backups by default (including any created before this change), and may additionally pass a namespace query parameter to GET/DELETE /data/files and GET /data/files/:filename to inspect or clean up a specific namespace's backups.

  • #18008 Data backup: a global administrator can now import or upload a namespaced backup by passing the namespace query parameter, consistent with listing and downloading. Previously, importing a namespaced backup directly failed while uploading it first (which silently moved it to the global scope) succeeded — the two now behave the same. Namespaced administrators remain confined to their own namespace on every operation.

  • #18164 Improved backup import feedback when working within a namespace. Importing an archive that does not belong to the target namespace -- for example one exported from a different namespace, or a global backup -- now returns a clear error instead of appearing to succeed while importing nothing. A global administrator can still restore a specific namespace's backup using the namespace query parameter.

Global backups are now complete cluster snapshots: a global export also includes every namespace's configuration, and a global import restores each namespace's configuration back into its own namespace. A cluster without namespaces produces and reads exactly the same archives as before.

  • #18204 Strengthened validation of data backup archives during import so a backup file's contents are restored only into the table it is meant for.

  • #18372 Make sure that backup file operations for a namespace always stay within that namespace's own backup directory. Backup operations are not available for a namespace whose name cannot be used as a directory name, such as ., .., or a name containing a path separator.

  • #18423 Data Backup imports performed by a namespaced administrator now apply only that namespace's configuration. Cluster-wide settings found in the namespaced configuration, such as authentication, authorization, ExHook, or listeners, are skipped with a warning instead of being written to the global configuration.

  • #18466 Fixed listing of backup files for namespaces whose names contain special characters.

Previously, the backup file list was empty for a namespace whose name contained characters such as *, ?, {, }, [ or ], even though the backup files existed on disk. The listing now treats the namespace name as a literal directory name.

  • #18677 Audit records for data-backup requests now identify the namespace a request targeted. Previously, exporting, importing, uploading, or deleting a backup in different namespaces produced audit records that looked identical, so it was not possible to tell which namespace's backup was affected. The audit log now also records any query parameters a request carried.

  • #18826 Backup import now confirms that every node in the cluster runs the same version before it starts.

Importing during a rolling upgrade could apply the backup through calls that the not-yet-upgraded nodes interpret differently. The import now stops before it begins and names the nodes still to be upgraded, so the cluster is left as it was.

  • #18886 Reject invalid Backup Sync interval and timeout values, and suppress false-positive TLS certificate path errors for successful HTTP synchronizations.

Multi-tenancy

  • #18227 Fixed an issue where clients of a deleted managed namespace could temporarily publish without namespace rate limits while asynchronous client kicking was in progress.

  • #18539 Fixed the multi-tenancy client list not following a persistent session that reconnects under a different namespace.

Previously, when a client resumed an existing session (clean_start=false) after its namespace changed, GET /api/v5/mt/ns/{ns}/client_list kept listing the client under the old namespace, and the new namespace's list did not include it. The client list and the per-namespace client count now always reflect the namespace the client connected with. This also fixes the client disappearing from the list after resuming a durable session.

  • #18774 Fixed an issue where clients could connect to a deleted multi-tenancy namespace while its resources were still being cleaned up.

Gateway

  • #17556 Fixed an issue where the OCPP gateway did not pass the listener enable_authn option to the shared authentication flow because the option was stored under a misspelled client-info key.

  • #17796 Fixed a crash in the MQTT-SN gateway when a new device connects from a UDP source port that was recently used by a disconnected device (common on loopback and behind NAT, where the OS or NAT box re-assigns the same port). The stale channel is now retired cleanly and the new connection is processed as a fresh session.

  • #17805 Fixed an issue where re-loading a gateway could fail with an already_started error after a previous load attempt aborted partway through (for example due to an invalid configuration or a busy listener port). The leftover locker process from the failed attempt is now reclaimed automatically, so the next load (or operator retry) starts from a clean state.

  • #17815 Fixed MQTT-SN UDP session routing when UDP source tuples change or are reused.

MQTT-SN UDP listeners now route packets by the ClientId parsed from the packet through esockd_udp_proxy, allowing asleep sessions to resume from a different UDP source tuple while preventing a reused UDP source tuple from delivering another ClientId's packets to the old session.

  • #18504 Fixed STOMP frame parsing of escaped header characters and CRLF line endings.

The STOMP gateway now decodes the escape sequences \c, \r, \n, and \\ in header names and values, as required by STOMP 1.2. CONNECT and CONNECTED frames are exempt: STOMP 1.2 excludes them from header escaping for backward compatibility with STOMP 1.0, so their headers, including a password containing a colon or a backslash, pass through unchanged. In other frames, an undefined escape sequence is now rejected as a frame error.

The gateway now also accepts CRLF (\r\n) line endings in frames and CRLF heartbeats. Before this fix, clients using CRLF line endings could not connect.

  • #18776 MQTT-SN now publishes configured Will messages when sleeping clients exceed their sleep duration and no longer publishes Will messages when clients disconnect normally.

  • #18842 Gateway connections now ignore clientid_override values returned by authentication backends, which is not supported by Gateway protocols. A warning is logged when this occurs.

Mountpoint templates for Gateway connections are now evaluated in the shared Gateway authentication flow after authentication results are merged.

Plugins

  • #17861 Restored the previous plugin startup behavior by no longer deleting local plugin packages that are missing from the cluster plugin configuration when a node starts or rejoins the cluster.

  • #17884 Fixed plugin management HTTP APIs to ignore stale unpacked plugin directories that are not present in the cluster plugin config and are not running locally.

Such stale packages no longer appear in plugin list/detail/config/schema responses, cannot be acted on by plugin operation APIs, and no longer block reinstalling the same package through the HTTP install API. Configured pre-installed plugins are still visible and continue to follow the documented pre-install workflow.

EMQX now logs an error on startup and HTTP API access when a plugin package is unpacked but is neither enabled nor disabled in plugins.states.

  • #17932 Fixed noisy failed_to_get_plugin_config_from_cluster warning when installing plugins via CLI.

The emqx ctl plugins install command now installs plugins in fresh_install mode (matching the HTTP API behavior), which skips the cluster config lookup for newly installed plugins, avoiding repeated config_not_found_on_node warnings on every node in the cluster.

Added --cluster flag to emqx ctl plugins install for cluster-wide installation. When specified, the plugin package is distributed to and installed on all running nodes in a single command.

  • #18018 Fixed plugin package installation loading code before validating the package's application declarations, configuration schema, and default configuration.

  • #18153 Fixed the plugin configuration API to return a readable validation error when the root JSON value has the wrong type, instead of returning 500 INTERNAL_ERROR.

  • #18333 Fixed plugin startup after a node restart for plugins that declare emqx_plugins in their application dependency list.

Plugins start while the plugin subsystem itself is starting. A plugin that declared emqx_plugins as a dependency made its own startup wait for the plugin subsystem, so the plugin start timed out and the plugin was left enabled but not running after every node restart. EMQX now ignores this dependency declaration and logs a warning that asks the plugin author to remove it.

When a plugin fails to start with a timeout, the error log now lists the declared dependency applications that were not running at that moment.

  • #18337 Start plugins after all EMQX applications have started. A plugin may now declare any EMQX application in its applications list. Previously, a plugin that declared an application which starts late in the boot sequence (for example emqx_management) failed to start after a node restart.

  • #18468 The hot-upgrade (relup) plugin now validates the target version string and checks upgrade-path compatibility before it modifies any files. An incompatible or malformed upgrade package is rejected without deleting or overwriting the installed release.

  • #18540 Ship the default configuration file (priv/config.hocon) in the emqx_relup plugin package. Installing the plugin no longer logs a repeated failed_to_copy_plugin_default_hocon_config warning.

  • #18891 Fixed an issue where installing and starting the Sync Request plugin on EMQX 6.x failed with a missing_i18n_ref error. The plugin now starts successfully and its API endpoint is available.

  • #18957 Fixed a bug where uploading a plugin package from the Dashboard replied ALREADY_INSTALLED and 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, replying 403 FORBIDDEN with the emqx ctl plugins allow instruction when the package is not authorized. Leftovers of an interrupted or failed installation (a directory without a readable release.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 with plugin_is_in_use and 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.

  • #19195 Hardened plugin package name validation.

REST API

  • #18069 Fixed the file transfer files API (GET /api/v5/file_transfer/files) failing with a 500 error when listing files whose names contain non-ASCII characters (e.g. Chinese).

  • #18114 Fixed an issue where the dashboard metrics APIs (GET /api/v5/monitor_current and GET /api/v5/monitor) returned 500 INTERNAL_ERROR while a node was joining the cluster.

While a joining node is restarting its applications, sampling its metrics fails; this failure is now tolerated: the APIs return the aggregate of the remaining reachable nodes and log a warning, instead of failing the whole request.

Also fixed a spurious clear_monitor_metrics_rpc_errors warning that was logged on every successful DELETE /api/v5/monitor request.

  • #18287 Improved REST API resilience when a cluster node becomes unreachable or fails while serving a request. A number of endpoints previously returned an opaque 500 error (or, in a few cases, reported success while part of the work had failed) when an RPC to a peer node did not complete; they now return a descriptive error response, and cluster-wide reads degrade gracefully to the results from the reachable nodes.

Affected areas include: listing and describing plugins, listing clients (v2), streaming and downloading trace logs, reading configuration in HOCON format from a specific node, deleting a delayed message on a specific node, resetting topic metrics, importing a data backup, per-node action/source operations, rule listing, file-transfer downloads, and deleting message queues. Retained-message reindexing and session takeover also now tolerate an unreachable peer node instead of aborting.

  • #18509 Fixed message paging in GET /clients/{clientid}/mqueue_messages and GET /clients/{clientid}/inflight_messages.

These APIs limit the total payload size of one response page by the max_payload_bytes parameter (default 1MB). When this limit cut a page short, the returned meta.position pointed past the messages that were left out, so requesting the next page from that position skipped them. This could look like lost messages, for example a mqueue_len count higher than the number of messages the API returns. Now meta.position points at the last returned message, and the next page continues with the first message that was left out.

  • #18544 Previously, when using GET /clients_v2 HTTP API to fetch all the clients using memory sessions, a cursor could be replied, which in turn would return then an empty page. Now, such cursor is no longer returned.

  • #18558 Previously, the fields query parameter of the GET /clients_v2 would not be honored.

  • #18600 Fixed client list API filtering when durable sessions are enabled.

GET /api/v5/clients appended all disconnected durable sessions to the result, ignoring query-string filters such as conn_state, username, and clientid. For example, conn_state=connected also returned disconnected durable sessions. Now the filters apply to disconnected durable sessions as well.

When filters are applied and disconnected durable sessions exist, the response omits the meta.count field instead of reporting a wrong number. Queries that filter with conn_state=connected keep an exact meta.count.

  • #18619 Fixed GET /nodes/{node} returning a 500 Internal Server Error instead of a 400 Bad Request when the target node becomes unreachable between the API's liveness check and the RPC that fetches its info, for example when the node concurrently leaves the cluster.

  • #18817 Fixed PUT /api/v5/telemetry/status returning 500 INTERNAL_ERROR with an Erlang stack trace when the request body omits the enable field.

The endpoint now returns 400 BAD_REQUEST with a validation message. The API documentation marks enable as required and no longer shows a default value for it, because the endpoint has never applied that default.

  • #18860 Fixed client message pagination returning a continuation position on the final page.

GET /clients/{clientid}/mqueue_messages and GET /clients/{clientid}/inflight_messages now return meta.position as end_of_data when no more messages are available. API clients can stop without requesting an additional empty page.

  • #19116 Improved the error returned when a REST API request carries bytes which are not text, such as a DER certificate file where a PEM file is expected. The response now names the reason and the size of the value. It used to quote the whole input back as a list of byte values, and to report that a binary value was not a binary.

Configuration values of a free-form type no longer keep a conversion error in place of such input.

Observability

  • #17708 Fixed a logger formatter crash that could replace some debug-level trace events with a FORMATTER CRASH line.

The crash happened when a log field held a tuple value (for example the result field of the authenticator_result and authentication_result authentication trace events) and the active formatter configuration did not carry a chars_limit setting. As a result, the events that show which authenticator accepted or rejected a connection were missing from the trace output. These events are now formatted correctly.

  • #17886 Exposed the publish quota-exceeded packet metric in Prometheus as emqx_packets_publish_quota_exceeded.

  • #18521 Identify the client in the connection shutdown report emitted when a connection exceeds a force-shutdown limit (force_shutdown.max_mailbox_size or force_shutdown.max_heap_size).

The shutdown reason now includes a label field. For an established connection, it holds the client ID. For a connection shut down before CONNECT completes, it holds the listener name and peer address. Previously the report contained only the limit and the measured value, so the operator could not tell which client was shut down.

  • #18684 Fixed a crash on connect when OpenTelemetry tracing was enabled and an MQTT 5 client's CONNECT packet carried will User-Property entries.

  • #18697 Fixed an issue where querying the audit log could return an error for records created by SSO-authenticated users.

  • #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.

  • #19148 The license_expiry alarm 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 a days_left field, and both the details and the message are kept up to date while the alarm stays active.

CLI

  • #18590 Fixed the output of emqx stop when the node is not running.

The command reported Node <name> not responding to pings. twice and then failed with Graceful shutdown failed PID=[]. It now reports the unreachable node once and does not print a shutdown failure for a node it could not find. The exit code is unchanged.

  • #18824 Fixed a misspelled field name in the emqx ctl listeners output.

The command printed the listener's enabled flag as enbale. It now prints enable. Scripts that parse this output must be updated to match the corrected name.

  • #18862 Validate the options passed to emqx_router_tool:scan_missing_routes/1 and emqx_router_tool:reconcile_missing_routes/1.

Invalid chunk or sleep_ms values were accepted silently and disabled the scan throttling, so the scan ran at full speed while the operator believed it was throttled. The tool now raises an error naming the offending option instead. Unknown option keys, such as a misspelled chunks, are rejected as well.

  • #19166 Fixed an issue where the emqx ctl clients stats command stopped with an error and left a partial CSV file when clients connected or disconnected while the command was running.

Deployment

  • #17877 Fixed the emqx-enterprise Helm chart hardcoding svc.cluster.local in the node's host name. On a Kubernetes cluster whose DNS domain is not cluster.local, a node named itself with an unresolvable FQDN, so Erlang distribution could not start and the nodes failed to form a cluster. The host name now follows the chart's clusterDomain value, which already governed the DNS and Kubernetes discovery settings.

  • #19049 Published the offline Docker image tarballs for each platform in the release download directory. The files are named emqx-enterprise-<version>-docker-amd64.tar.gz and emqx-enterprise-<version>-docker-arm64.tar.gz.

Stopped publishing the Snowflake ODBC Docker images (tags with the -sf suffix). Existing -sf tags on Docker Hub stay available, but new releases do not get one.

Source: README.md, updated 2026-09-29