An MQTT connection can fail at four layers: the network (DNS and TCP), the transport (TLS or WebSocket), the MQTT handshake (CONNECT / CONNACK) and the live session (keep-alive and disconnects). Identifying the layer is half the fix. This guide walks through each one, with the exact error strings and reason codes you are likely to see.
Quick triage
| Symptom | Layer | Most likely cause |
|---|---|---|
ENOTFOUND / getaddrinfo error | DNS | Typo in the hostname, or a URL scheme included in the host field |
| Hangs, then times out | Network | Firewall dropping the port, wrong port, or TCP port used from a browser |
ECONNREFUSED | Network | Nothing listening on that host and port |
| Certificate or handshake error | TLS | Untrusted CA, hostname mismatch, or TLS against a plain port |
| Connection closed right after connect | MQTT | CONNACK with an error code — see the tables below |
| Connects, then drops repeatedly | Session | Duplicate client ID or keep-alive misconfiguration |
Connection timeouts
A timeout means your packets went out and nothing came back. The broker never got the chance to say no. Check, in order:
- The port matches the transport.
mqtt://on 1883,mqtts://on 8883,wss://on the broker's WebSocket port. See MQTT ports explained. - Your network allows the port. Corporate and guest networks often block 1883 and 8883. Try
wss://on 443 to confirm. - The broker is up. Public sandboxes restart without notice; test another broker to rule out your own setup.
- Your server firewall is open. For self-hosted brokers, the cloud security group must allow inbound traffic on the listener port.
ECONNREFUSED
ECONNREFUSED is a fast, explicit rejection from the operating system: the host is reachable, but no process is listening on that port. Typical reasons are a broker that is not running, a listener bound only to 127.0.0.1, a container port that was never published (docker run -p 1883:1883), or connecting to localhost from inside a container where the broker lives in a different one.
TLS errors
- Self-signed or unknown CA (
SELF_SIGNED_CERT_IN_CHAIN,UNABLE_TO_VERIFY_LEAF_SIGNATURE): give the client the CA file. Do not disable verification outside of local testing. - Hostname mismatch (
ERR_TLS_CERT_ALTNAME_INVALID): connect using the name on the certificate, not an IP address or internal alias. - Wrong version number / packet length too long: you are speaking TLS to a plain-text port (for example
mqtts://on 1883) or plain MQTT to a TLS port. - Expired certificate: check the dates with
openssl s_client -connect host:8883 -servername host.
Browser-only errors: mixed content and WebSocket paths
Browsers can only use MQTT over WebSockets, and they add their own rules. A page served over HTTPS cannot open ws:// — the browser blocks it as mixed content before any packet is sent, and the error only shows up in the developer console. Use wss://. The other frequent mistake is a missing path: many brokers expect /mqtt, and without it the WebSocket upgrade fails with a 404. Read more in MQTT over WebSockets.
Test a known-good wss connection
MQTT 3.1.1 CONNACK return codes
If the network and transport work, the broker answers CONNECT with CONNACK. In MQTT 3.1.1 it carries a one-byte return code:
| Code | Meaning | What to fix |
|---|---|---|
0 | Connection accepted | Nothing — you are connected |
1 | Unacceptable protocol version | Broker does not support the requested version; switch between MQTT 3.1.1 and 5 |
2 | Identifier rejected | Client ID is invalid, too long, empty when not allowed, or blocked |
3 | Server unavailable | Broker is overloaded or in maintenance; retry with backoff |
4 | Bad user name or password | Check credentials and their encoding |
5 | Not authorized | Account is not allowed to connect; check broker ACLs or the client ID rule |
Common MQTT 5 reason codes
MQTT 5 replaces return codes with reason codes. Anything at or above 0x80 is a failure. The same codes can appear in CONNACK or in a server-sent DISCONNECT:
| Code | Name | Typical cause |
|---|---|---|
0x80 | Unspecified error | Generic failure; check broker logs |
0x84 | Unsupported Protocol Version | Broker only speaks 3.1.1 |
0x85 | Client Identifier not valid | Malformed or disallowed client ID |
0x86 | Bad User Name or Password | Wrong credentials |
0x87 | Not authorized | Valid user without permission to connect |
0x88 | Server unavailable | Broker cannot accept connections right now |
0x8A | Banned | Client or IP blocked by the operator |
0x8C | Bad authentication method | Enhanced auth method not supported |
0x95 | Packet too large | CONNECT exceeds the broker's maximum packet size |
0x97 | Quota exceeded | Account hit a connection or usage limit |
0x9F | Connection rate exceeded | Reconnecting too fast; add backoff |
Two more show up in DISCONNECT packets during a session: 0x8D (Keep Alive timeout) and 0x8E (Session taken over). Both are covered below. For the bigger picture of what MQTT 5 changed, see MQTT 5 vs 3.1.1.
Client ID takeover loops
The spec requires a broker to disconnect an existing client when a new connection arrives with the same client ID. If two devices — or two browser tabs — share an ID and both auto-reconnect, they take turns kicking each other off every few seconds. In MQTT 5 the loser receives DISCONNECT with 0x8E; in 3.1.1 the socket simply closes.
Keep-alive disconnects
The keep-alive value in CONNECT tells the broker how often to expect traffic. If it sees nothing for one and a half times that interval, it closes the connection. Clients send PINGREQ when idle, but a blocked event loop, a sleeping device or an aggressive NAT or load balancer idle timeout can still break the connection. Keep the interval below your network's idle timeout (60 seconds is a common default) and never block the thread that services the MQTT client.
Log the real error
Most libraries hide the reason unless you listen for it. With MQTT.js:
import mqtt from 'mqtt';
const client = mqtt.connect('wss://broker.emqx.io:8084/mqtt', {
protocolVersion: 5,
clientId: `debug-${Math.random().toString(16).slice(2, 10)}`,
reconnectPeriod: 5000,
});
client.on('connect', (connack) => console.log('connected', connack.reasonCode ?? 0));
client.on('error', (err) => console.error('error:', err.message, err.code));
client.on('disconnect', (packet) => console.warn('server disconnect, reason', packet.reasonCode));
client.on('close', () => console.warn('socket closed'));Checklist
- Confirm the round trip on a known-good public broker in the online client.
- Match scheme, port and path to the broker's documented listener.
- Read the CONNACK code or reason code instead of guessing.
- Use a unique client ID and reconnect with backoff.
- Keep keep-alive shorter than any idle timeout on the path.
For a full test procedure, follow how to test an MQTT broker.
Frequently asked questions
What does MQTT connection refused not authorized mean?
The broker accepted the network connection but rejected the client. In MQTT 3.1.1 this is CONNACK return code 5, in MQTT 5 reason code 0x87. The credentials may be valid, but the account is not allowed to connect or use that client ID.
Why does my MQTT client keep disconnecting and reconnecting?
The most common cause is two clients using the same client ID. The broker disconnects the older session whenever a new one connects with that ID, and if both auto-reconnect they kick each other off in a loop. Give every client a unique ID.
Why does my MQTT connection time out instead of failing?
A timeout means no response at all, which usually points to a firewall silently dropping packets, a wrong host, or using the TCP port from a browser. A refusal, by contrast, means something answered and said no.