github emqx/emqx 6.3.0
EMQX Enterprise (LTS) 6.3.0

5 hours ago

EMQX Enterprise 6.3.0 focuses on stronger security defaults, leaner deployments, and broader operational visibility.

Highlights include:

  • Feature Gates. Set EMQX_FEATURES=ESSENTIAL to run the core MQTT broker with an under-100 MB boot-time memory footprint;
  • Security Profile. Set EMQX_SECURITY_PROFILE=hardened for secure-by-default deployments;
  • A more performant socket backend for MQTT TCP listeners;
  • Refined QoS 0 flow control for slow or congested subscribers;
  • Topic Metrics v2 with wildcard matching and Prometheus scraping;
  • Authenticated OpenAPI and Prometheus endpoints;
  • Stronger credential hashing;
  • Improved MQTT session handling and rate limiting;
  • Bigtable integration.

In summary, this release includes 39 enhancements and 47 bug fixes comparing to version 6.2.3

Download

Ubuntu / Debian

OS Arch Package Tarball
ubuntu24.04 amd64 .deb (sha256) .tar.gz (sha256)
ubuntu24.04 arm64 .deb (sha256) .tar.gz (sha256)
ubuntu22.04 amd64 .deb (sha256) .tar.gz (sha256)
ubuntu22.04 arm64 .deb (sha256) .tar.gz (sha256)
debian13 amd64 .deb (sha256) .tar.gz (sha256)
debian13 arm64 .deb (sha256) .tar.gz (sha256)
debian12 amd64 .deb (sha256) .tar.gz (sha256)
debian12 arm64 .deb (sha256) .tar.gz (sha256)
debian11 amd64 .deb (sha256) .tar.gz (sha256)
debian11 arm64 .deb (sha256) .tar.gz (sha256)

RHEL / Rocky / Amazon Linux

OS Arch Package Tarball
el10 amd64 .rpm (sha256) .tar.gz (sha256)
el10 arm64 .rpm (sha256) .tar.gz (sha256)
el9 amd64 .rpm (sha256) .tar.gz (sha256)
el9 arm64 .rpm (sha256) .tar.gz (sha256)
el8 amd64 .rpm (sha256) .tar.gz (sha256)
el8 arm64 .rpm (sha256) .tar.gz (sha256)
amzn2023 amd64 .rpm (sha256) .tar.gz (sha256)
amzn2023 arm64 .rpm (sha256) .tar.gz (sha256)
el7 amd64 .rpm (sha256) .tar.gz (sha256)

macOS

OS Arch Package
macos14 arm64 .zip (sha256)
macos15 arm64 .zip (sha256)
macos26 arm64 .zip (sha256)

Plugins

Plugin Version Package
emqx_acme 0.2.0 .tar.gz (sha256)
emqx_agent 1.0.0 .tar.gz (sha256)
emqx_backup_sync 0.1.3 .tar.gz (sha256)
emqx_bridge_mqtt_dq 0.5.2 .tar.gz (sha256)
emqx_maptabs 0.1.2 .tar.gz (sha256)
emqx_offline_messages 2.0.1 .tar.gz (sha256)
emqx_relup 1.0.2 .tar.gz (sha256)
emqx_sync_request 0.1.1 .tar.gz (sha256)
emqx_unsgov 0.1.4 .tar.gz (sha256)
emqx_username_quota 1.2.3 .tar.gz (sha256)

Breaking Changes

  • #17185 The MQTT parser now runs in strict mode by default. To restore the previous lenient behavior, set mqtt.strict_mode = false (globally or per-zone).

    In strict mode, the broker validates incoming MQTT packets against the protocol specification and disconnects clients that send malformed packets. The validations enforced only in strict mode are:

    • Fixed-header flags. Reserved DUP/QoS/RETAIN bits must be zero for non-PUBLISH packets, and PUBREL/SUBSCRIBE/UNSUBSCRIBE must use QoS=1 (bad_frame_header).
    • CONNECT reserved bit must be zero (reserved_connect_flag).
    • CONNECT Will flag consistency: Will Flag=0 requires Will QoS=0 and Will Retain=0; Will Flag=1 requires Will QoS in {0,1,2} (invalid_will_qos, invalid_will_retain).
    • CONNECT Password/Username flags (MQTT 3.1.1 only). If Username Flag=0, Password Flag must also be 0, per [MQTT-3.1.2-22] (invalid_password_flag). MQTT 5.0 lifts this constraint and is unaffected.
    • UTF-8 strings (proto name, client ID, topic, username, password, will topic, MQTT 5 string properties) must be valid UTF-8 and must not contain control characters U+0000–U+001F or U+007F–U+009F (utf8_string_invalid).
    • Packet identifiers must be non-zero where required (PUBLISH QoS>0, PUBACK/REC/REL/COMP, SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK) (bad_packet_id).

    When a client violates one of these checks, the broker logs an info-level entry with msg=frame_parse_error and a structured reason (for example, cause=invalid_password_flag, proto_ver, or received_prefix) for troubleshooting. For MQTT 5.0 connections, the broker also responds with CONNACK/DISCONNECT carrying reason code 0x81 Malformed Packet before closing; for MQTT 3.1/3.1.1, the connection is silently closed (no CONNACK reason code is defined for malformed packets in those versions).

  • #17215 Removed the bundled Swagger UI assets from the EMQX release package, reducing tarball size by approximately 11 MB.

    /api-docs/swagger.json continues to serve the full OpenAPI 3 JSON spec, so external Swagger UI deployments that load it by URL keep working. The legacy /api-docs URL responds with an HTTP 308 redirect to /api-spec.html, the in-tree spec explorer introduced in 6.3.0. Other /api-docs/* subpaths (the embedded Swagger UI assets) are no longer served and return 404.

  • #17267 The node.max_ports config now defaults to auto, which scales the Erlang VM port limit (+Q) with the number of logical CPU cores: 65536 ports per core for up to 8 cores, and 1048576 (the historical fixed default) above that. Explicit integer values are still accepted.

    This is a behavior change for nodes upgraded from earlier versions where max_ports defaulted to a fixed 1048576: hosts with 8 or fewer CPU cores will now boot with a smaller port table. Setups that rely on accepting more than cores * 65536 connections must set node.max_ports explicitly (and restart the node) before upgrading.

    The hidden node.process_limit setting is reinstated as an override: when set to a value larger than the derived limit (2 * max_ports), it is respected; smaller values are ignored so the process table never under-sizes the port table.

    A new node.schedulers setting (default auto) controls the Erlang scheduler count (+S). With auto, the count is capped at the number of logical processors actually available to the VM (sched_getaffinity on Linux), so containers limited via --cpuset-cpus or Kubernetes CPU requests no longer spawn scheduler OS threads they cannot run in parallel. Set it to a positive integer to override the auto-detected value.

  • #17437 Prometheus scrape endpoints (/api/v5/prometheus/*) now require authentication by default. Set prometheus.enable_basic_auth = false explicitly to restore the previous unauthenticated behavior. Deployments that scrape these endpoints without credentials will need to either configure credentials on the scraper or set the config field. The recommended setup is a dedicated API key with the monitoring scope, used with Bearer auth in the scraper.

  • #17582 Prometheus VM and Mnesia collector metric names now use the prometheus.erl 6.x promtool-compliant names.

    Affected metric renames:

    • erlang_mnesia_failed_transactions -> erlang_mnesia_failed_transactions_total
    • erlang_mnesia_committed_transactions -> erlang_mnesia_committed_transactions_total
    • erlang_mnesia_logged_transactions -> erlang_mnesia_logged_transactions_total
    • erlang_mnesia_restarted_transactions -> erlang_mnesia_restarted_transactions_total
    • erlang_vm_memory_atom_bytes_total -> erlang_vm_memory_atom_bytes
    • erlang_vm_memory_bytes_total -> erlang_vm_memory_bytes
    • erlang_vm_memory_processes_bytes_total -> erlang_vm_memory_processes_bytes
    • erlang_vm_memory_system_bytes_total -> erlang_vm_memory_system_bytes
    • erlang_vm_statistics_context_switches -> erlang_vm_statistics_context_switches_total
    • erlang_vm_statistics_garbage_collection_number_of_gcs -> erlang_vm_statistics_garbage_collection_number_of_gcs_total
    • erlang_vm_statistics_garbage_collection_words_reclaimed -> erlang_vm_statistics_garbage_collection_words_reclaimed_total
    • erlang_vm_statistics_garbage_collection_bytes_reclaimed -> erlang_vm_statistics_garbage_collection_bytes_reclaimed_total
    • erlang_vm_statistics_runtime_milliseconds -> erlang_vm_statistics_runtime_seconds_total
    • erlang_vm_statistics_wallclock_time_milliseconds -> erlang_vm_statistics_wallclock_time_seconds_total
    • erlang_vm_port_count -> erlang_vm_ports
    • erlang_vm_process_count -> erlang_vm_processes
    • erlang_vm_atom_count -> erlang_vm_atoms
  • #17596 Added authorization options that forbid interpolation of /, +, and # symbols into topic filter templates in authorization rules. The new options are:

    authorization.topic_template_allow {
      plus = false,
      hash = false,
      slash = false
    }

    With false, the corresponding symbol cannot be used in a value interpolated into a topic template. For example, if plus = false, then username bad+user is forbidden in a rule such as {allow, all, publish, ["userspace/${username}"]}. The outcome depends on the active security profile: with the legacy profile the rule will not match, and with the hardened profile the action will be denied.

  • #17677 Dropped support for the JSON output format in the Prometheus REST API.

    The endpoints under /api/v5/prometheus (stats, auth, data_integration, schema_validation, message_transformation) now only produce the Prometheus text format. Requests sending Accept: application/json are rejected with 400 Bad Request ("only prometheus format is supported"); previously they returned a JSON representation of the metrics.

  • #17626 #18123 Added a new configuration multi_tenancy.deny_namespaces holding namespace names that cannot be used as a namespace identifier, either as an admin namespace (dashboard roles, API keys, multi-tenancy management API) or as a per-client client_attrs.tns; a client whose client_attrs.tns resolves to a denied name is rejected.

    This is a breaking change: the default value ["global", "undefined", "null", "none"] denies names that were previously accepted. These names collide with internal sentinels and would produce ambiguous log lines and dashboard output. Existing namespaces with these names are not migrated; rename them before upgrading, or set multi_tenancy.deny_namespaces to an empty list to lift the restriction.

    Additionally, when multi_tenancy.post_auth_tns_expression is configured and evaluates to an empty value or fails to evaluate, a client whose pre-authentication client_attrs.tns is a denied namespace name is now also rejected, consistent with the handling when the expression evaluates to a non-empty value.

  • #18228 The default authorization rules file (acl.conf) no longer grants clients connecting from 127.0.0.1 blanket publish/subscribe access to all topics (including $SYS/# and #).

    Clients connecting from localhost are now authorized by the same rules as any other client, and ultimately by the authorization.no_match setting. In particular, subscriptions to $SYS/# and the wildcard filters # and +/# are now denied for localhost clients by the default rules, regardless of the security profile.

    Deployments that relied on the built-in localhost allowance must add an explicit rule to acl.conf. The previous rule is retained in the file as a comment for easy re-enabling:

    %% {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}.

    Note: this applies to new installations and deployments that have not customized acl.conf; existing customized acl.conf files are not modified by upgrades.

  • #18244 The ExProto gateway has been removed.

  • #18271 #18329 MQTT and gateway WebSocket listeners no longer read the client address and port from forwarded headers by default: the default value of proxy_address_header and proxy_port_header changed from x-forwarded-for / x-forwarded-port to empty, meaning the socket source address and port are always used. Deployments behind load balancers or reverse proxies that rely on forwarded headers must now configure the header names explicitly (for example, set proxy_address_header to x-forwarded-for). Setting an empty header name disables the forwarded-header lookup.

    This change also fixes the forwarded-header lookup for gateway WebSocket listeners. Previously, a configured header name was never matched against the request headers, so the socket source address and port were used even when the forwarded headers were present.

  • #18377 Managed namespace names are now validated when created. A name may contain only ASCII letters, digits, and the characters ., -, and _, with a length of 1 to 255 bytes; the names . and .. are not accepted. Namespaces that already exist are not affected.

  • #18390 The mqtt.clientid_override expression no longer falls back to the client-supplied Client ID when it fails.

    When mqtt.clientid_override is configured and the expression raises an error (for example, it references an attribute the client did not provide) or renders an empty string, EMQX now refuses the connection with CONNACK reason code 0x85 (Client Identifier not valid; return code 2 for MQTT 3.1 and 3.1.1 clients). Previously such clients stayed connected under their original Client ID, so the override silently did not apply to them.

    Before upgrading, verify that every connecting client can render the configured expression to a non-empty string. Clients that could not render the expression connected with their original Client ID before the upgrade; after the upgrade they are refused until the expression or the client data is fixed.

  • #18419 Removed the Google Cloud IoT Core migration compatibility feature, including the GCP Device authenticator and device management APIs.

  • #18515 Updated the Azure Blob Storage Action's blob template field to use the same schema validation as the Aggregated S3 Action's key field. The validation rejects unsupported template bindings.

  • #18528 Added validation that requires the exporter endpoint of an OpenTelemetry integration to be a URL with an explicit scheme and port. Supported schemes are http and https.

  • #18627 Dashboard SAML SSO now verifies IdP signatures by default in all security profiles.

    Previously the default followed the security profile: the hardened profile verified signatures, but the legacy profile (the default until v7.0) did not, so it accepted an unsigned, forged SAMLResponse and issued a Dashboard session.

    If you intentionally run an unsigned IdP, set sso.saml.idp_signs_envelopes = false and sso.saml.idp_signs_assertions = false explicitly. If the IdP does sign but its metadata carries no certificate, the SAML backend now fails to start with missing_idp_certificate.

Enhancements

Core MQTT Functionalities

  • #16694 Added support for extracting peer certificates from QUIC connections, enabling peer_cert_as_username on QUIC mTLS listeners.

  • #17307 Added a per-client rate limiter for SUBSCRIBE packets on listeners. It is disabled by default. When configured with a finite rate, EMQX responds to packets that exceed the limit with a SUBACK containing the Quota Exceeded reason code and does not process them. Namespaces can configure independent rates, which override the listener-level rate.

  • #17546 #18477 Added the mqtt.max_session_expiry_interval configuration to cap the session expiry interval an MQTT 5.0 client may request via its Session-Expiry-Interval property. If the client requests longer than this limit, the server silently clamps it and reflects the clamped value back in the CONNACK. The setting defaults to infinity (no clamp), preserving the previous behavior. It has no effect on MQTT 3.1.1/3.1 clients, whose session expiry remains fully server-controlled via mqtt.session_expiry_interval.

    The cap also applies to the Session Expiry Interval a client supplies in a DISCONNECT packet, so a client cannot extend its session expiry beyond the configured limit when disconnecting.

  • #17603 Added support for extracting subject alternative names from directly connected TLS client certificates into MQTT client attributes with cert_san.dns, cert_san.ip, cert_san.email, and cert_san.uri in mqtt.client_attrs_init.

  • #17854 Changed the default tcp_backend for MQTT TCP listeners to socket on Unix systems to improve message latency and resource usage. The gen_tcp backend remains available by setting tcp_backend = gen_tcp, and remains the default on Windows.

  • #17870 Improved in-memory session delivery behavior for slow or congested subscribers.

    • EMQX now tracks connection send-queue congestion and moves QoS 0 deliveries through the session message queue when needed, instead of continuing to push them directly to a congested connection.
    • The session message queue now prefers evicting older QoS 0 messages when it reaches capacity, helping QoS 1 and QoS 2 deliveries make progress during QoS 0 bursts.
    • Fixed delivery ordering when a session delivery rate limit is reached, so later messages do not overtake earlier queued messages.
    • Improved socket-backed connection send_timeout handling so the timeout is armed only after the socket queue is past its watermark.

Access Control

  • #17145 Authorization sources now support Variform-based preconditions. A source with a precondition is called only when the expression evaluates to true, allowing different authorization backends to be selected by client and request context such as client attributes, action, and topic.

  • #17487 Strengthened Dashboard administrator password and API key secret hashing.

    Dashboard administrator passwords are now hashed with PBKDF2-HMAC-SHA256 (600,000 iterations) and a 16-byte random salt. API key secrets are hashed in the same self-describing storage format with the same 16-byte random salt but without iteration stretching, so per-request HTTP API authentication remains cheap. The previous scheme (single-pass SHA-256 with a 2-byte salt) is replaced for both credential types.

    Existing stored hashes continue to be accepted for authentication. They are rewritten in the new format the next time the user changes their password or the API key is recreated.

  • #17671 Authentication and authorization rejection logs now include per-backend attribution.

    When EMQX rejects a client because an authenticator returns an error, it emits a warning log identifying the authenticator ID and provider that produced the rejection. When an authorization source denies an operation, EMQX logs the denial at warning level (previously only visible under a client trace) with the source type, module, topic, and action.

    This makes it possible to tell which backend produced a decision in deployments with multiple authenticators or authorization sources, without having to enable a client trace first. The new logs are throttled per authenticator and per authorization source to avoid log floods.

  • #18130 Added support for template variables in the URL host of HTTP authenticators and authorizers, for example https://${client_attrs.tns}.auth.example.com/authn, enabling per-tenant authentication and authorization endpoints without an external routing layer.

    The hostname_resolution setting controls how the URL host is handled. The default static preserves the previous behavior: a fixed hostname and a persistent connection pool established at configuration time. Setting it to dynamic (required when the URL host contains template placeholders) makes EMQX resolve the hostname for every request and send each check over a per-request connection, applying the configured TLS options (including peer verification) to that host; the pool_size setting then limits how many idle connections may be kept for reuse (0 disables reuse), while pipelining options do not apply.

    For templated hosts, the new allowed_hosts setting must list the hostnames the host may render to, either as exact names or as *.suffix wildcard patterns; when the rendered hostname is not covered by this list, no request is made and the check fails. URLs with a literal hostname keep using the connection pool exactly as before.

  • #18239 #18371 Flapping detection can now also act on the username and the source IP address of connecting clients, in addition to the client ID.

    Each dimension has its own detection window, connect-attempt threshold, and ban duration, configured under flapping_detect.by_clientid, flapping_detect.by_username, and flapping_detect.by_peerhost (username and source IP detection are disabled by default). The client ID dimension was previously configured with the flat flapping_detect fields (enable, window_time, max_count, ban_time); these are now deprecated but remain accepted and are automatically mapped onto by_clientid, so existing configurations keep working unchanged. When a username or source IP address exceeds its threshold within the detection window, it is temporarily banned: new connection attempts are rejected before authentication runs, while already-connected clients are not affected. Ban entries expire automatically and can be inspected or removed early via the /banned REST API; each entry carries the ban type (as: clientid, username, or peerhost) and by = flapping detector, and the list can be filtered by type.

    Counters are kept separately per zone and per dimension, so zones configured with different detection windows do not discard each other's still-active counters.

    New metrics: flapping.detected.clientid, flapping.detected.username, and flapping.detected.peerhost count flapping triggers per dimension, and client.banned counts connection attempts rejected due to an active ban entry.

Multi-tenancy

  • #17454 Scoped Prometheus data returned by /api/v5/prometheus/data_integration to the requesting actor's namespace. A global administrator can view data from all namespaces and filter by namespace.

Data Integration

  • #17129 Added Attached Service Account authentication for GCP-based connectors (GCP PubSub Producer, GCP PubSub Consumer, and BigQuery). When EMQX runs on a GCP VM with a service account attached to the instance, it can query the internal metadata endpoint to obtain a token for these connectors.

  • #17222 Added a Bigtable data integration that supports appending data to Bigtable.

  • #17547 Added support for AWS IAM Roles Anywhere in Kafka Producer and Consumer Connectors. The Connectors can be configured with the HTTP endpoint exposed by the credential helper process.

    The credential helper process must be running and accessible to EMQX. See the AWS IAM Roles Anywhere credential helper documentation for more information.

  • #17783 Added an application_name option to PostgreSQL-family connectors. It defaults to emqx and is sent as the PostgreSQL startup parameter so connector sessions can be identified in PostgreSQL activity views and logs. The value must be 1 to 63 bytes long and cannot contain zero bytes.

  • #18119 The Disk Log connector now supports time-based file rotation in addition to size-based rotation.

    A new optional rotation setting was added to the connector configuration:

    • rotation.period: none (default), day, or hour. When set to day or hour, the connector starts a separate set of log files at each period boundary, with the period's date stamp (YYYYMMDDHH) encoded in the file names (for example, mqtt-trace-2026062400.log.1 for daily rotation of mqtt-trace.log). Size-based rotation (max_file_size / max_file_number) still applies within each period.
    • rotation.retention_period: how long to keep files from previous periods (for example, 30d); date-stamped files outside the retention period are deleted automatically after each period rotation. Defaults to infinity (files are retained indefinitely).
    • rotation.timezone: timezone used to determine period boundaries: UTC (default), local, or a fixed offset such as +02:00.

    The default behavior (no rotation configured, or rotation.period = none) is unchanged.

  • #18319 Added support for specifying an action-specific project ID for BigQuery Actions.

  • #18624 Added emqx ctl actions show and emqx ctl actions status commands. They report action status for the local node only, in JSON, without REST API credentials or a network call.

    status prints a compact JSON array of {"<type>:<name>": "<status>"} entries; show prints the same information as GET /api/v5/actions/{id}, with connector secrets redacted, but for the local node only. Both accept --name <type:name> to select one action and --ns <namespace> to select a namespace, defaulting to every action in the global namespace.

    This suits a per-node readiness probe, where the REST API's cluster-aggregated status field cannot tell whether the local node's own actions are ready to accept traffic.

Observability

  • #17493 Added session buffer observability: client APIs now report total_payload_bytes, sysmon.session.total_payload_bytes_high_watermark can emit throttled warnings for sessions over a payload-byte threshold, and emqx ctl session-top can export the cluster top sessions by total_payload_bytes or mqueue_length.

  • #17582 Updated Prometheus metrics collection to use prometheus.erl version 6.1.2, improving performance and scalability.

    The vm_dist, vm_statistics, vm_system_info, and vm_memory collectors are now enabled by default. The obsolete emqx_vm_process_messages_in_queues metric was removed from Prometheus output.

  • #17607 #17998 Added a v2 topic-metrics surface with named collections, wildcard topic filters, namespace ownership, REST CRUD and a Prometheus scrape endpoint.

    • New routes under /api/v5/mqtt/topic_metrics2/:name let operators register topic-metric collections by user-chosen name (my-pressure, vehicle-events, …) instead of using the topic itself as the identifier. Wildcards (t/#, sensor/+/temp) are now accepted as topic filters, and a single published message may match and increment several collections.
    • Collections are namespace-scoped: a collection created by a namespaced admin only counts publishers whose client_attrs.tns matches. Global collections (created by a non-namespaced admin) count every publisher. Namespaced admins see and modify only their own collections. A global administrator can address an individual namespaced collection on the per-collection endpoints (GET, DELETE, PUT .../reset) by passing an ns query parameter; a namespaced admin passing another namespace's name is rejected with 403 Forbidden, and omitting ns keeps the actor's own namespace.
    • Counters are exposed in Prometheus exposition format at /api/v5/prometheus/topic_metrics with labels name, topic_filter, namespace. Rates can be derived via Prometheus rate().
    • The v1 API (/api/v5/mqtt/topic_metrics and /api/v5/mqtt/topic_metrics/:topic) is unchanged and continues to work; it is marked deprecated in its Swagger spec, and integrations should use v2.
  • #18148 Added support for Dynatrace integration via OpenTelemetry. Supported signals are traces and logs. The integration uses OAuth2 tokens for authentication.

Deployment and Security

  • #17381 The OpenAPI specification endpoints now require authentication by default. This covers GET /api-docs/swagger.json, GET /api-spec.json, GET /api-spec.md, and GET /api-spec/:tag[/:name].

    Unauthenticated requests receive a 401 with a WWW-Authenticate header and a minimal but valid OpenAPI document (or its Markdown equivalent for /api-spec.md) that lists the supported security schemes and the public bootstrap endpoints (POST /api/v5/login, GET /api/v5/status), so callers can discover how to authenticate without the dashboard exposing its full API surface anonymously.

    The dashboard's api-spec.html explorer continues to load anonymously and fetches the spec with the existing session cookie or token.

  • #17407 #17808 Added Feature Gates.

    Added support for starting EMQX with a limited set of features specified by the EMQX_FEATURES environment variable. Invalid presets or feature names prevent the node from booting. Dependent features are enabled automatically.

    There are two presets available:

    • FULL: the default. Starts EMQX with all available features.
    • ESSENTIAL: starts EMQX with the minimum feature set: the core MQTT broker with authentication and authorization.

    The available features are:

    • dashboard: Dashboard UI (including SSO and RBAC), REST API.
    • data_integration: Connectors, Actions, Sources, and Rule Engine.
    • message_transformation: Message transformation.
    • schema_validation: Schema validation.
    • schema_registry: Schema registry.
    • gateways: Gateway protocols.
    • cluster_link: Cluster linking.
    • multi_tenancy: Multi-tenancy and namespacing.
    • ai: AI features (A2A registry, AI completion).
    • metrics: Prometheus metrics exporting.
    • mqtt_extensions: MQTT extensions: delayed publish, topic rewrite, Auto Subscribe, Slow Subscriptions, message queue, and streams.
    • plugins: Plugin framework for installing and managing third-party plugins.

    The following features cannot be enabled by themselves and are only enabled when using the full preset:

    • file_transfer: File transfer extension to MQTT.
    • exhook: External gRPC hooks.
    • opentelemetry: OpenTelemetry exporter.
  • #17768 Added support for sourcing node.cookie from a file using the file:// URL form.

    Operators can now set node.cookie = "file:///path/to/cookie" (or point the EMQX_NODE__COOKIE environment variable at a file:// URL) so the cluster secret is not stored as plain text in the configuration. The referenced path may be a regular file or a FIFO (named pipe); it is read once when the node boots. When a FIFO is used, the orchestrator must write the cookie to it on each boot, before any other emqx command is invoked (such as emqx ctl), because later commands obtain the cookie from the already-running node rather than re-reading the file.

    The resolved cookie is now passed to the Erlang VM directly and is no longer written to the generated data/configs/vm.*.args file, so the secret is not persisted to disk during boot.

  • #17803 When EMQX is booted with EMQX_FEATURES=ESSENTIAL, the Erlang code loading mode now defaults to interactive so that the .beam files of disabled features are loaded on demand instead of all at boot. This significantly reduces the resident memory footprint of an essential-mode node, since the modules belonging to skipped features never become resident. The mode can still be overridden by setting CODE_LOADING_MODE explicitly.

  • #18451 Added support for reading boot-time environment variables from etc/emqx.env (/etc/emqx/emqx.env on RPM and DEB installations, /opt/emqx/etc/emqx.env in the Docker image).

    The file lists EMQX_FEATURES and EMQX_SECURITY_PROFILE with their defaults commented out and a description of what each one does. These variables are read before emqx.conf is parsed, so they cannot be set in emqx.conf. The emqx command sources the file on every invocation, so a service start, a foreground start, and emqx ctl all see the same values. Values in the file override the inherited environment. Package upgrades keep edits to the file.

  • #18452 Added security_profile and feature_preset to the node information returned by GET /nodes and GET /nodes/{node}.

    security_profile is legacy or hardened. feature_preset is full, essential, or custom. Both values are fixed when the node boots, so the list view shows when nodes in a cluster run with different settings. Stopped nodes do not report these fields.

  • #18453 Added the security_profile_divergence alarm.

    Nodes running the hardened security profile (EMQX_SECURITY_PROFILE) periodically check the security profile of the other running nodes in the cluster, and raise the alarm when another running node runs the legacy profile. Nodes running the legacy profile do not run the check, and nodes running an older EMQX release without security profiles count as legacy. The alarm message names the legacy nodes, and the alarm details keep the current node list while the alarm is active. The alarm clears on its own once every running node runs the hardened profile, or once the last legacy node leaves the cluster.

    The alarm is expected for a short time during a rolling upgrade that changes the security profile. An alarm that stays active points to nodes that were not restarted with the new EMQX_SECURITY_PROFILE value.

  • #18471 The node_dump diagnostic script now includes the EMQX_FEATURES and EMQX_SECURITY_PROFILE settings from the boot-time environment file etc/emqx.env, when the file exists. Other variables in the file are not collected.

  • #18557 #18609 Added the node.default_listener_address configuration option. It sets the address of MQTT listeners, gateway listeners, and the Dashboard HTTP listener when their bind has no explicit address, such as a bare-port bind (bind = 1883). Valid values: loopback (bind 127.0.0.1), nodename (bind the address in the host part of the Erlang node name, resolving it first when it is not an IP address), all (bind 0.0.0.0), a literal IPv4/IPv6 address, or a hostname to resolve at boot. When the option is not set, the security profile decides the default address as before. An explicit IP:port bind always wins. The option can also be set with the EMQX_NODE__DEFAULT_LISTENER_ADDRESS environment variable.

    The official Docker image sets EMQX_NODE__DEFAULT_LISTENER_ADDRESS=all, so defaulted listeners stay reachable through published container ports regardless of the security profile.

    Listener views now report resolved_address: the IP address a listener is actually bound to on the node it runs on, alongside the existing bind field. bind keeps showing the configured value, including the port; resolved_address shows the IP bind resolves to after the security profile or node.default_listener_address applies, without the port, which can differ from the address in bind for a bare-port bind such as bind = 1883.

    A second field, resolved_address_from, reports why resolved_address has its value: bind when the listener's own bind already sets an explicit address, 0.0.0.0 for all interfaces, 127.0.0.1 for loopback, nodename, or the literal node.default_listener_address value when it is a hostname or IP address.

    Both fields are node-local: GET /api/v5/listeners/:id reports the values for the node handling the request, and GET /api/v5/listeners reports them per node under node_status, since a listener with the same ID can resolve to different addresses on different nodes in the same cluster, for example when node.default_listener_address is set to nodename.

    emqx ctl listeners prints both fields alongside the existing listen_on.

  • #18628 Data backup exports now record the exporting node's security profile.

    Restoring a backup that was exported under the legacy security profile onto a node running the hardened security profile can carry over data and configuration that behaves differently once restored:

    • Bare-port MQTT, gateway, and Dashboard HTTP listener binds resolve to loopback instead of all interfaces.
    • An authenticator chain left empty or disabled starts denying every client instead of allowing them.
    • A restored Dashboard account still on the default password can no longer log in.
    • Authentication and authorization backend failures that were previously ignored now deny the operation.

    Importing such a backup now needs the --allow-security-profile-mismatch CLI flag, or the allow_security_profile_mismatch API parameter, so an operator gets a chance to review these differences instead of discovering them after clients stop connecting or logins start failing. A backup that predates this change is treated the same as one exported under legacy. Restoring into a node running legacy is never affected.

Plugins

  • #18455 Under the hardened security profile, emqx ctl plugins allow <Name-Vsn> now requires the sha256:<hex> argument. The grant binds the plugin package to the given SHA-256 digest. Only an upload whose bytes match the digest is installed. A grant without a digest is refused with a message that shows the required command. A node running the hardened profile also refuses a grant without a digest that a cluster peer sends to it.

    The legacy security profile is unchanged: the sha256:<hex> argument stays optional.

Packaging

  • #17335 After installation from an RPM or DEB package, the directory /opt/emqx is created and populated with convenience symlinks (bin, data, etc, lib, log, plugins, releases, erts-*) pointing at the scattered FHS paths used by the package. Operators can now use the same /opt/emqx/... paths as in the official Docker image, regardless of how EMQX was installed.

Performance

  • #17583 Improved JSON encoding and decoding performance.

    As part of this change, floating-point numbers in JSON output are now formatted consistently with the Erlang/OTP standard, which may differ slightly from previous releases (for example, switching to scientific notation a little earlier).

  • #18033 In ESSENTIAL feature mode (and any deployment with the dashboard/management client-info API disabled), EMQX no longer performs the periodic per-connection statistics reporting that only feeds the GET /clients endpoint, reducing per-connection overhead at high connection counts.

  • #18424 Plugin configuration schemas (config_schema.avsc) are now read from the installed plugin package each time a plugin configuration is validated, instead of being kept in memory for every installed plugin.

    When the schema file of an installed plugin is missing or unreadable, plugin configuration validation now reports the file error.

  • #18688 The number of dirty I/O scheduler threads (+SDio) can now be configured via node.dirty_io_schedulers. The default auto keeps the previous fixed value of 8 on nodes where node.schedulers resolves to more than 2, and uses 4 on smaller nodes (for example, a 2 vCPU container or cgroup). This reduces boot-time memory footprint on small nodes while keeping enough threads to overlap blocking I/O operations.

Bug Fixes

Core MQTT Functionalities

  • #18010 Malformed MQTT packets sent by clients are no longer logged as broker errors.

    Such invalid input is now counted in the connection shutdown counters of the listener, reducing alert noise from port scanners, protocol fuzzers and misbehaving clients. Parse errors that carry packet-specific detail share a single frame_error counter, so malformed packets can no longer create new counter names.

    Details of the parse error, including the offending bytes, are reported through tracing: start a trace on the client ID, IP address or topic to inspect them.

  • #18027 Changed shared-subscription handling so EMQX disconnects clients that attempt a shared subscription while shared subscriptions are disabled.

    When mqtt.shared_subscription is set to false and a client sends a SUBSCRIBE containing a shared topic filter ($share/... or $queue/...), EMQX now closes the network connection, as required by the MQTT specification for protocol errors. MQTT 5.0 clients first receive a DISCONNECT packet with reason code 0x9E (Shared Subscriptions not supported); MQTT 3.1/3.1.1 clients simply get the connection closed.

    Previously such a SUBSCRIBE was answered with a failure reason code in the SUBACK and the connection was kept open.

  • #18116 When strict_mode is enabled (the default), an MQTT v5 packet that includes a non-repeatable property more than once (for example, two Session-Expiry-Interval properties in a CONNECT packet) is now rejected as a protocol error, instead of silently using the last value. User-Property, which the MQTT specification allows to repeat, is not affected.

    Operators can restore the previous lenient behavior by setting strict_mode = false in the listener configuration.

  • #18438 Fixed an off-by-one in the size check applied to outgoing packets.

    A packet whose serialized size was exactly the client's Maximum Packet Size was discarded and logged as frame_is_too_large, instead of being delivered. Only packets larger than the limit are now discarded, which is what MQTT 5.0 requires. Packets below the limit are unaffected.

  • #18470 Fixed an issue where EMQX acknowledged but did not deliver a retransmitted QoS 2 PUBLISH packet when the original packet had not been received before the publisher disconnected.

    This fix partially reverts the QoS 2 duplicate-handling change introduced in #16721. After the awaiting-PUBREL state expires, a retransmitted QoS 2 PUBLISH packet is again treated as a new QoS 2 exchange and may be delivered to subscribers more than once. Starting with EMQX 7.0.0, awaiting-PUBREL state expiration will be disabled by default, preventing such redelivery.

  • #18487 Reduced log volume for PUBACK, PUBREC, PUBREL, and PUBCOMP packets that carry an unknown Packet Identifier. These events are logged at debug level and remain available through client tracing.

  • #18523 Changed shutdown ordering so EMQX stops MQTT listeners before stopping applications.

    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 Fixed session takeover so EMQX ends a session that does not outlive its connection when another 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.

Access Control

  • #18246 Added delayed-message authorization to the hardened security profile.

    • In the hardened security profile, delayed messages are reauthorized when replayed. Delayed messages from MQTT and gateway clients carry a restricted authorization context. EMQX checks current publish authorization rules and ban records before replay. A message that was authorized when scheduled can be dropped when replayed.
    • In the hardened security profile, pending delayed messages created before the upgrade are dropped when replayed because they do not contain an authorization context. The legacy profile continues to replay them.
    • Fixed mountpoint handling in multiple gateways. Gateways consistently pass logical, unmounted topics to authorization. When authorization.include_mountpoint = false, EMQX checks the logical topic. When it is true, EMQX applies the mountpoint once for the authorization check. In both cases, EMQX applies the mountpoint once before publishing or subscribing.
    • The GBT 32960, JT/T 808, LwM2M, NATS, and STOMP gateways no longer pass pre-mounted topics to publish authorization. This prevents authorization from checking a double-mounted topic when authorization.include_mountpoint = true.
    • GBT 32960 dnstream, JT/T 808 proto.dn_topic, and LwM2M command auto-subscriptions no longer apply the mountpoint before authorization.
    • JT/T 808 proto.up_topic and proto.dn_topic are now relative to the gateway mountpoint. Their defaults changed from jt808/${clientid}/${phone}/up and jt808/${clientid}/${phone}/dn to ${phone}/up and ${phone}/dn.
    • MQTT-SN idle QoS -1 publishes and will messages now apply the configured mountpoint; these paths previously published without it.
    • NATS publish authorization checks the MQTT topic converted from the NATS subject before applying the mountpoint. NATS JWT permissions and EMQX authorization no longer check a pre-mounted topic.
    • Fixed duplicate processing of delayed messages by other systems, such as bridges, the retainer, and the schema validator. EMQX processes delayed messages only during actual replay. Direct internal publishers (for example, plugins) must invoke the message.ingress hook to schedule delayed messages. Direct emqx:publish/1 calls and management API publishes to $delayed/... bypass message ingress and fail to schedule delayed messages.
  • #18458 Upgraded oidcc to 3.2.3.

    The upgrade fixed Dashboard SSO (OIDC) login timeouts (INTERNAL_ERROR: exit,{timeout,{gen_server,call,[...]}}) while the provider configuration worker was busy refreshing its cached configuration.

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

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

Multi-tenancy

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

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

Data Integration

  • #18300 Blank certificate file fields in a connector's TLS settings are now treated as not configured regardless of the verify mode. Previously, blank client certificate fields were rejected with a validation error when verify was set to verify_peer, although client certificates are optional for peer verification.

  • #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 Fixed a rare race condition in which the PostgreSQL Action encountered a sock_closed error while writing data and incorrectly treated it as unrecoverable. EMQX treats the error as recoverable.

  • #18767 Fixed the RocketMQ connector being reported as belonging to a namespace it does not belong to.

    The RocketMQ connector has its own namespace configuration field, holding the RocketMQ instance namespace. Connector API responses returned that value under the same JSON key used for the EMQX namespace, so the Dashboard treated the connector as owned by a namespace of that name. It showed "Only the administrator of namespace can perform operations on the connector", and opening the connector failed with "Managed namespace not found".

    The namespace field in connector API responses now always holds the EMQX namespace. The RocketMQ instance namespace is no longer returned, and it is kept unchanged when a connector is updated without it.

Rule Engine

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

Clustering

  • #18409 Fixed Cluster Linking for a link whose server field lists more than one address.

    Such a link now connects through all listed addresses: each connection prefers one of the addresses in turn and fails over to the others when it cannot connect. Before this change the link could not connect, and no link could be created, updated or deleted until the node was restarted.

  • #18447 Fixed an issue where changes to base.hocon could be ignored after a node synchronized configuration from another cluster member. Configuration synchronization no longer persists the peer's base.hocon values into cluster.hocon, so local base.hocon changes take effect after restart unless explicitly overridden by cluster configuration.

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

Gateway

  • #18312 Fixed an issue in plaintext CoAP UDP listeners with connection mode enabled where a rejected request from another source could redirect subsequent downlink messages.

  • #18436 Fixed an issue where NATS Gateway internal JWT authentication did not enforce account JWT exp/nbf claims or account-level user revocations.

    Expired or not-yet-valid account JWTs and user JWTs revoked by the account are now rejected during authentication. Existing connections are disconnected when the earlier of the user JWT or account JWT expiration is reached. Malformed resolver-preloaded account JWTs are rejected during gateway configuration validation.

  • #18494 Fixed CoAP gateway clients reporting the internal keepalive check interval instead of the configured heartbeat interval.

    The client information returned by the gateway API and emqx ctl gateway-clients list coap now reports the configured heartbeat value in seconds.

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

  • #18700 Fixed an issue where saving an existing NATS Gateway configuration from the Dashboard could replace its authentication credentials with the masked value displayed in the form, causing NATS clients to fail authentication.

    Authentication credentials are now preserved when users update other Gateway settings without changing the authentication configuration.

    NATS Gateway authentication settings now reject duplicate authentication methods and credential entries, including duplicate NKeys and JWT account entries, to prevent ambiguous authentication behavior.

Plugins

  • #18188 Hardened the plugin framework package and runtime integrity checks.

    • Operators must run emqx ctl plugins allow before emqx ctl plugins install, enforcing the same allow gate as the HTTP upload API.
    • Plugin API callback responses are restricted to an allow-list of safe response headers; browser-sensitive headers (such as set-cookie, location, access-control-*, content-security-policy, and other authentication or security policy headers) and custom headers without the x-plugin- prefix are stripped.
    • Added the plugins.package_limits configuration to bound plugin package extraction: max_package_size (default 10MB), max_decompressed_size (default 50MB), max_file_count (default 10000), max_path_depth (default 32), and max_extraction_time_ms (default 60s, also used as the RPC timeout for cluster package copying). Packages violating these limits are rejected before or during extraction. Tar entries escaping the install directory (path traversal) are also rejected.
  • #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 Shipped 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.

ExHook

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

  • #18473 Fixed ExHook authentication and authorization behavior when no callback server is running. The legacy security profile now honors the configured failed_action, while the hardened security profile remains fail-closed.

Observability

  • #17602 Added configuration-backed emqx ctl log outputs commands so CLI changes to logger outputs stay consistent with logger configuration managed by the HTTP API and Dashboard.

  • #17912 Fixed a security vulnerability in OpenTelemetry W3C Baggage header extraction (GHSA-64w2-whjg-q7q7). Previously, the inbound baggage header was decoded with no byte or entry-count limit, and a malformed key-value pair would crash the process. Extraction is now capped at 8192 bytes and 180 entries as recommended by the W3C Baggage specification, and malformed pairs are skipped instead of crashing.

  • #18521 Added an identifying label to the connection shutdown report emitted when a connection exceeds a force-shutdown limit (force_shutdown.max_mailbox_size or force_shutdown.max_heap_size).

    For an established connection, the label 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 identify the affected connection.

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

Management

  • #18289 Fixed an issue where JSON-compatible Unicode escape sequences in quoted HOCON strings and keys were left escaped instead of being decoded.

  • #18403 Fixed emqx ctl commands printing non-ASCII characters as \x{...} escapes or invalid bytes when the command runs in a shell without a UTF-8 locale (for example over non-interactive SSH, cron, or LANG unset).

    The Erlang VMs started by the emqx script (the node itself, emqx ctl, emqx eval, emqx remote_console, and emqx escript) now always read and write standard I/O as UTF-8.

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

  • #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 Fixed GET /clients_v2 returning a cursor after returning all clients with memory sessions. Following that cursor returned an empty page. The API no longer returns a cursor when no more data is available.

  • #18558 Fixed GET /clients_v2 ignoring the fields query parameter.

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

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

Deployment and Security

  • #17921 Upgraded the protobuf dependency to v0.17.0. This dependency is used only for SBOM generation and is not part of the EMQX runtime. The upgrade picks up a fix for an unbounded-recursion denial of service when decoding deeply-nested messages (GHSA-rv48-qqj5-crxg) and Elixir 1.19/1.20 compiler warning fixes, and replaces the previously pinned development ref with the official release.

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

Don't miss a new emqx release

NewReleases is sending notifications on new releases.