MQTT over WebSockets: Diagnose Upgrade Failures and Proxy Idle Disconnects
Separate an MQTT-over-WebSocket failure into four checkpoints: a valid HTTP upgrade, selection of the MQTT subprotocol, correct WebSocket transport of MQTT bytes, and MQTT session acceptance. After those succeed, investigate idle disconnects by the direction of missing traffic and the timer that observes it. A successful WebSocket connection alone does not establish an accepted MQTT session.
This procedure uses the MQTT 5.0 Standard of 7 March 2019, section 6, and RFC 6455, December 2011. The reverse-proxy example is explicitly NGINX 1.26.2 over HTTP/1.1. HTTP/2 extended CONNECT, TLS provisioning and general reconnect policy are outside scope. Source and configuration semantics were inspected; no live WebSocket or NGINX experiment was executed.
Collect a layered connection record
For one synthetic test connection, correlate the client attempt, proxy request and broker connection. Retain the requested path, upstream route, HTTP result, selected subprotocol, first MQTT CONNECT and CONNACK outcomes, and final close evidence. Redact credentials, cookies and private payloads before exporting traces.
Record timestamps with enough context to distinguish total connection age from time since the last byte in each direction. An idle timer usually observes a particular traffic gap; “the connection lasted about a minute” is only a symptom.

First establish that an upgrade actually happened
For the scoped HTTP/1.1 route, verify the request reaches the intended WebSocket listener and the response is 101 with valid upgrade-handshake fields. A normal HTTP 200 page, redirect or authentication rejection does not complete that handshake.
Check the response’s Upgrade, Connection and Sec-WebSocket-Accept fields through the WebSocket library’s supported validation. Do not weaken acceptance to “status 101” while ignoring the rest of the handshake. RFC 6455 also constrains extensions and any selected subprotocol to what the client offered.
A failed HTTP status does not uniquely identify the component responsible. Compare the proxy’s upstream status and route with the broker log. A wrong path, rewritten location or upstream listener mismatch needs route evidence, while an authorization rejection needs the responsible service’s policy evidence.
Apply version-specific proxy settings
The NGINX WebSocket guide explains that Upgrade and Connection are hop-by-hop headers and require explicit handling across a reverse proxy. Confirm the effective location forwards the intended upgrade headers to the backend and that the backend returns the switching response.
For NGINX 1.26.2, explicitly configure proxy_http_version 1.1 for this route. The tagged proxy-module source defaults that directive to HTTP/1.0. Current rolling documentation has changed its example for newer releases, so copying it without matching the installed version can omit a setting needed here.
Inspect the fully loaded configuration rather than one proposed snippet. Included files, a more specific location or an ingress controller’s generated configuration can make the effective route different from the file under review.
Then verify the MQTT-specific negotiation
MQTT 5 requires the client to offer mqtt in Sec-WebSocket-Protocol and the server to select mqtt. A generic WebSocket echo endpoint can satisfy the basic upgrade while failing this protocol contract.
Once negotiated, MQTT control packets travel in WebSocket binary data frames. Sending a JSON representation or text-encoded hexadecimal packet is not equivalent. The MQTT rule about other data-frame types does not prohibit WebSocket Ping, Pong or Close control frames.
Do not require one MQTT packet per WebSocket frame. Section 6 explicitly allows a frame to contain multiple or partial MQTT packets. The receiver needs correct WebSocket processing followed by incremental MQTT stream parsing. Test frame and network-read boundaries independently of MQTT Remaining Length.
Keep session rejection at the MQTT layer
After the transport is established, verify that the broker receives CONNECT and that the client processes CONNACK. Preserve its MQTT reason code. A WebSocket upgrade cannot prove that the MQTT credentials, client settings or session request were accepted.
If no CONNACK arrives, locate the last successful boundary: binary data reached the proxy, reached the listener, decoded as MQTT, or entered broker authentication. Avoid treating every missing acknowledgement as a TLS or WebSocket handshake problem.
Diagnose idle timeouts by direction
NGINX documents proxy_read_timeout as a gap between reads from the proxied server, rather than a total response-duration budget. The 1.26.2 source initializes its default to 60,000 milliseconds. An inherited or explicit configuration can override it.
In a hypothetical deployment, suppose that gap is sixty seconds and an otherwise idle MQTT client schedules its first PINGREQ near a negotiated 120-second Keep Alive limit. The proxy can close the quiet upstream connection first. These numbers illustrate conflicting settings; they are not measured behavior or a claim that all clients wait until the limit.
Client-to-broker QoS 0 publications alone need not produce broker-to-proxy bytes. They therefore cannot be assumed to refresh an upstream-read timer. MQTT PINGRESP, delivered publications or server-generated WebSocket Ping frames may provide traffic in the relevant direction, depending on the actual implementation.
MQTT Keep Alive and WebSocket Ping/Pong are different mechanisms. Qualify an intentional heartbeat and timeout relationship with scheduling margin, then verify it in capture and logs. Simply increasing every timeout hides the evidence without establishing which layer was closing the connection.
Preserve close evidence and test one cause at a time
Record an MQTT DISCONNECT reason, a received WebSocket Close status, and a TCP termination separately when available. RFC 6455 reserves 1006 for abnormal-closure reporting; it is not a status an endpoint may transmit in a Close frame. A client reporting 1006 does not reveal a specific broker reason.
- Establish a direct authorized laboratory WebSocket connection, then repeat through the proxy.
- Verify the exact route, complete handshake and mqtt subprotocol before sending CONNECT.
- Exercise binary MQTT packets split across frames and multiple packets in a frame.
- Run an idle case while recording last upstream traffic and effective read timeout.
- Add the chosen heartbeat, verify its direction, and repeat beyond the former failure point.
- Inject a controlled MQTT rejection and WebSocket close separately to check diagnostic classification.
These are proposed deployment tests, not executed results. Release evidence should identify the first failed layer and the specific timer or rejection involved, with a reproduction that changes when that cause is corrected.