To use MQTT over WebSockets in Python, create the paho client with transport="websockets", connect to the broker's WebSocket port and, if needed, set the URL path with ws_set_options(). For encrypted wss, add tls_set() and use the secure WebSocket port. The MQTT API is otherwise identical: same callbacks, same publish() and subscribe().
This guide shows ws and wss connections, custom paths and headers, connecting through HTTP and SOCKS proxies, and how to debug the handshake errors paho raises.
When WebSockets make sense from Python
Browsers can only speak MQTT over WebSockets, but Python can use plain TCP, which is simpler and slightly lighter. Pick WebSockets in Python when:
- Your network only allows outbound HTTP(S), for example a corporate firewall or a hotel network that blocks 1883 and 8883 (see MQTT behind a corporate firewall).
- The broker sits behind a reverse proxy or load balancer that only routes HTTP, such as Nginx or a cloud ingress.
- You want a backend script to use exactly the same endpoint as your web dashboard, to reproduce a browser problem.
If none of these apply, TCP on 1883/8883 is the default choice. The overhead difference is measured in TCP vs WebSocket performance.
Plain WebSockets (ws://)
import time
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, reason_code, properties):
print("CONNACK:", reason_code)
client.subscribe("testmqtt/py-ws/#")
def on_message(client, userdata, msg):
print(msg.topic, msg.payload.decode())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, transport="websockets")
client.ws_set_options(path="/mqtt") # /mqtt is also paho's default
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 8083, keepalive=60)
client.loop_start()
client.publish("testmqtt/py-ws/hello", "hello over ws", qos=1)
time.sleep(2)
client.loop_stop()
client.disconnect()transport must be set in the constructor; you cannot switch an existing client from TCP to WebSockets. Valid values are "tcp", "websockets" and "unix"; anything else raises ValueError: transport must be "websockets", "tcp" or "unix", not ....
Secure WebSockets (wss://)
For anything beyond a quick test, use wss. It is the same code plus tls_set(), the secure port and real credentials:
import os
import paho.mqtt.client as mqtt
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id="py-wss-demo",
transport="websockets",
protocol=mqtt.MQTTv5,
)
client.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
client.tls_set() # verify the certificate and hostname
client.ws_set_options(path="/mqtt")
client.on_connect = lambda c, u, f, rc, p: print("CONNACK:", rc)
client.connect("broker.connectmq.com", 8084, keepalive=60)
client.loop_forever()paho performs the TLS handshake first, then the HTTP upgrade inside the encrypted channel, then MQTT CONNECT. Every TLS option and error from MQTT over TLS in Python applies unchanged.
Paths and custom headers
The WebSocket path must match what the broker or proxy expects, and there is no universal standard. /mqtt is the most common (it is the default for EMQX, HiveMQ and many managed brokers), RabbitMQ's Web MQTT plugin uses /ws, and reverse proxies can map any path you choose. A wrong path is the number one cause of WebSocket connection failures; see WebSocket path 404.
ws_set_options() also accepts headers, either as a dict that is merged into the defaults or as a function that receives the default headers and returns the final set:
import os
TOKEN = os.environ["GATEWAY_TOKEN"]
def add_headers(headers):
headers["Authorization"] = f"Bearer {TOKEN}" # for an auth proxy in front of the broker
headers["Origin"] = "https://dashboard.example.com"
return headers
client.ws_set_options(path="/mqtt", headers=add_headers)By default paho sends Origin: http://host:port (or https:// with TLS) and Sec-WebSocket-Protocol: mqtt. Overriding Origin matters when a proxy enforces an allow-list, a topic covered in CORS and WebSocket origin checks. Remember that MQTT credentials still go in the CONNECT packet via username_pw_set(); headers are only for HTTP-level components in front of the broker.
Connecting through an HTTP or SOCKS proxy
paho tunnels through proxies with the PySocks library. Install it first with pip install pysocks, then configure the proxy explicitly:
import socks # from the PySocks package
import paho.mqtt.client as mqtt
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, transport="websockets")
client.tls_set()
client.proxy_set(proxy_type=socks.HTTP, proxy_addr="proxy.corp.local", proxy_port=3128)
client.connect("broker.connectmq.com", 8084)
client.loop_forever()With socks.HTTP, PySocks opens an HTTP CONNECT tunnel to broker.connectmq.com:8084, and paho runs TLS, the WebSocket upgrade and MQTT through it. Use socks.SOCKS5 (optionally with proxy_username / proxy_password) for a SOCKS proxy.
Two details save a lot of time:
- paho ignores
HTTPS_PROXYandHTTP_PROXY. The only environment variable it reads ismqtt_proxy, for exampleexport mqtt_proxy=http://proxy.corp.local:3128orsocks://host:1080, and only when PySocks is installed. Hosts inno_proxyare bypassed. - Many corporate proxies only allow
CONNECTto port 443. If the tunnel is refused, you need a broker endpoint on 443 or a proxy exception.
Handshake errors and how to fix them
WebSocket problems surface as WebsocketConnectionError, a subclass of ConnectionError. With connect() it is raised immediately; with connect_async() plus loop_start() paho retries quietly, so enable client.enable_logger() while debugging.
| Error | Likely cause | Fix |
|---|---|---|
WebSocket handshake error, connection not upgraded | HTTP answer without upgrade: wrong path (404), plain web server, proxy error page | Check the path; make sure host and port point at the WebSocket listener |
WebSocket handshake error | Connection closed during the upgrade, or a non-HTTP answer (often plain ws to a TLS port) | Use tls_set() with wss ports; check the port is a WebSocket listener |
WebSocket handshake error, invalid secret key | A proxy or server returned a wrong Sec-WebSocket-Accept | Bypass or fix the intercepting proxy |
[SSL: WRONG_VERSION_NUMBER] wrong version number | tls_set() against a plain ws port such as 8083 | Use 8084 for wss, or drop tls_set() |
ConnectionRefusedError: [Errno 61] Connection refused (111 on Linux) | Nothing listens on that port, or a firewall rejects it | Verify the port; test from another network |
CONNACK Not authorized / Bad user name or password | The WebSocket works, MQTT auth failed | Check credentials and ACLs |
Look up MQTT reason codes from the last row with the reason code lookup.
How to test the connection
Start by checking the HTTP upgrade without Python. A healthy WebSocket endpoint answers 101 Switching Protocols:
curl -i -N --max-time 5 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Protocol: mqtt" \
https://broker.connectmq.com:8084/mqttA 404 means the path is wrong, a 400 or 426 usually means a header was stripped, and a TLS error means you are talking to the wrong port. Then do an end-to-end check: open the online MQTT client, connect over wss to the same broker, subscribe to testmqtt/py-ws/# and run ws_client.py. If the browser receives your message, Python, the path and the broker are all correct. Testing a WebSocket MQTT connection has more checks, including proxies and load balancers.
Subscribe in the browser, publish from Python
Using aiomqtt instead
If your code is asyncio-based, aiomqtt (which wraps paho) exposes the same options as constructor arguments:
import asyncio
import ssl
import aiomqtt
async def main():
async with aiomqtt.Client(
"broker.connectmq.com",
8084,
transport="websockets",
websocket_path="/mqtt",
tls_context=ssl.create_default_context(),
) as client:
await client.subscribe("testmqtt/py-ws/#")
await client.publish("testmqtt/py-ws/hello", "hello from aiomqtt")
async for message in client.messages:
print(message.topic, message.payload)
break
asyncio.run(main())MQTT over WebSockets in Python is one constructor argument plus the right port and path. Use wss with verification on, set the path explicitly so nobody has to guess it, configure proxies with proxy_set() or mqtt_proxy, and test the upgrade with curl before blaming your code.
Frequently asked questions
Which port and path does paho-mqtt use for WebSockets?
paho uses whatever port you pass to connect() and the path /mqtt by default. Plain WebSockets are commonly on 8083 or 80, secure WebSockets on 8084 or 443. Change the path with client.ws_set_options(path=...) if your broker or proxy uses a different one.
Does paho-mqtt respect HTTPS_PROXY or HTTP_PROXY?
No. paho only reads an environment variable named mqtt_proxy (with an http:// or socks:// URL) and only if PySocks is installed. Otherwise configure the proxy explicitly with client.proxy_set(proxy_type=socks.HTTP, proxy_addr=..., proxy_port=...).
What does WebSocket handshake error, connection not upgraded mean?
The server answered the HTTP upgrade request but did not switch to WebSocket. Usually the path is wrong (a 404 page), you hit a normal web server or proxy instead of the broker's WebSocket listener, or a proxy stripped the Upgrade header.


