Para usar MQTT sobre WebSockets en Python, crea el cliente paho con transport="websockets", conéctate al puerto WebSocket del broker y, si hace falta, define el path de la URL con ws_set_options(). Para wss cifrado, añade tls_set() y usa el puerto de WebSocket seguro. Por lo demás, la API MQTT es idéntica: los mismos callbacks, el mismo publish() y el mismo subscribe().
Esta guía muestra conexiones ws y wss, paths y cabeceras personalizados, la conexión a través de proxies HTTP y SOCKS, y cómo depurar los errores de handshake que lanza paho.
Cuándo tienen sentido los WebSockets desde Python
Los navegadores solo pueden hablar MQTT por WebSockets, pero Python puede usar TCP directo, que es más simple y algo más ligero. Elige WebSockets en Python cuando:
- Tu red solo permite HTTP(S) saliente, por ejemplo un firewall corporativo o la red de un hotel que bloquea el 1883 y el 8883 (consulta MQTT detrás de un firewall corporativo).
- El broker está detrás de un reverse proxy o balanceador que solo enruta HTTP, como Nginx o un ingress en la nube.
- Quieres que un script de backend use exactamente el mismo endpoint que tu dashboard web, para reproducir un problema del navegador.
Si nada de esto aplica, TCP en 1883/8883 es la opción por defecto. La diferencia de overhead está medida en rendimiento TCP vs WebSocket.
WebSockets sin cifrar (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 es también el valor por defecto de paho
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 debe definirse en el constructor; no puedes cambiar un cliente existente de TCP a WebSockets. Los valores válidos son "tcp", "websockets" y "unix"; cualquier otro lanza ValueError: transport must be "websockets", "tcp" or "unix", not ....
WebSockets seguros (wss://)
Para cualquier cosa más allá de una prueba rápida, usa wss. Es el mismo código más tls_set(), el puerto seguro y credenciales reales:
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() # verifica el certificado y el 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 realiza primero el handshake TLS, luego el upgrade HTTP dentro del canal cifrado y después el CONNECT de MQTT. Todas las opciones y errores TLS de MQTT sobre TLS en Python se aplican sin cambios.
Paths y cabeceras personalizadas
El path del WebSocket debe coincidir con lo que espera el broker o el proxy, y no hay un estándar universal. /mqtt es el más común (es el valor por defecto en EMQX, HiveMQ y muchos brokers gestionados), el plugin Web MQTT de RabbitMQ usa /ws, y los reverse proxies pueden mapear el path que elijas. Un path incorrecto es la causa número uno de fallos de conexión WebSocket; consulta WebSocket path 404.
ws_set_options() también acepta cabeceras, ya sea como un dict que se fusiona con las predeterminadas o como una función que recibe las cabeceras por defecto y devuelve el conjunto final:
import os
TOKEN = os.environ["GATEWAY_TOKEN"]
def add_headers(headers):
headers["Authorization"] = f"Bearer {TOKEN}" # para un proxy de autenticación delante del broker
headers["Origin"] = "https://dashboard.example.com"
return headers
client.ws_set_options(path="/mqtt", headers=add_headers)Por defecto paho envía Origin: http://host:port (o https:// con TLS) y Sec-WebSocket-Protocol: mqtt. Sobrescribir Origin importa cuando un proxy aplica una lista de orígenes permitidos, tema que se trata en CORS y comprobación de origen en WebSocket. Recuerda que las credenciales MQTT siguen yendo en el paquete CONNECT mediante username_pw_set(); las cabeceras solo sirven para componentes HTTP situados delante del broker.
Conectar a través de un proxy HTTP o SOCKS
paho atraviesa proxies con la librería PySocks. Instálala primero con pip install pysocks y configura el proxy explícitamente:
import socks # del paquete PySocks
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()Con socks.HTTP, PySocks abre un túnel HTTP CONNECT hacia broker.connectmq.com:8084, y paho ejecuta TLS, el upgrade WebSocket y MQTT a través de él. Usa socks.SOCKS5 (opcionalmente con proxy_username / proxy_password) para un proxy SOCKS.
Dos detalles ahorran mucho tiempo:
- paho ignora
HTTPS_PROXYyHTTP_PROXY. La única variable de entorno que lee esmqtt_proxy, por ejemploexport mqtt_proxy=http://proxy.corp.local:3128osocks://host:1080, y solo si PySocks está instalado. Los hosts enno_proxyse excluyen. - Muchos proxies corporativos solo permiten
CONNECTal puerto 443. Si el túnel se rechaza, necesitas un endpoint del broker en el 443 o una excepción en el proxy.
Errores de handshake y cómo corregirlos
Los problemas de WebSocket aparecen como WebsocketConnectionError, una subclase de ConnectionError. Con connect() se lanza de inmediato; con connect_async() más loop_start() paho reintenta en silencio, así que activa client.enable_logger() mientras depuras.
| Error | Causa probable | Solución |
|---|---|---|
WebSocket handshake error, connection not upgraded | Respuesta HTTP sin upgrade: path incorrecto (404), servidor web normal, página de error del proxy | Revisa el path; asegúrate de que host y puerto apuntan al listener WebSocket |
WebSocket handshake error | Conexión cerrada durante el upgrade o respuesta que no es HTTP (a menudo ws sin cifrar contra un puerto TLS) | Usa tls_set() con puertos wss; comprueba que el puerto es un listener WebSocket |
WebSocket handshake error, invalid secret key | Un proxy o servidor devolvió un Sec-WebSocket-Accept incorrecto | Evita o corrige el proxy que intercepta |
[SSL: WRONG_VERSION_NUMBER] wrong version number | tls_set() contra un puerto ws sin cifrar como el 8083 | Usa el 8084 para wss, o quita tls_set() |
ConnectionRefusedError: [Errno 61] Connection refused (111 en Linux) | Nada escucha en ese puerto, o un firewall lo rechaza | Verifica el puerto; prueba desde otra red |
CONNACK Not authorized / Bad user name or password | El WebSocket funciona, falló la autenticación MQTT | Revisa credenciales y ACL |
Consulta los reason codes MQTT de la última fila con el buscador de reason codes.
Cómo probar la conexión
Empieza comprobando el upgrade HTTP sin Python. Un endpoint WebSocket sano responde 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/mqttUn 404 significa que el path es incorrecto, un 400 o 426 suele indicar que se eliminó una cabecera, y un error TLS significa que estás hablando con el puerto equivocado. Después haz una prueba de extremo a extremo: abre el cliente MQTT online, conéctate por wss al mismo broker, suscríbete a testmqtt/py-ws/# y ejecuta ws_client.py. Si el navegador recibe tu mensaje, Python, el path y el broker están bien. En probar una conexión MQTT por WebSocket tienes más comprobaciones, incluidos proxies y balanceadores.
Suscríbete en el navegador y publica desde Python
Usar aiomqtt en su lugar
Si tu código se basa en asyncio, aiomqtt (que envuelve paho) expone las mismas opciones como argumentos del constructor:
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 sobre WebSockets en Python es un argumento del constructor más el puerto y el path correctos. Usa wss con la verificación activa, define el path de forma explícita para que nadie tenga que adivinarlo, configura los proxies con proxy_set() o mqtt_proxy y prueba el upgrade con curl antes de culpar a tu código.
Preguntas frecuentes
¿Qué puerto y qué path usa paho-mqtt para WebSockets?
paho usa el puerto que pases a connect() y, por defecto, el path /mqtt. Los WebSockets sin cifrar suelen estar en el 8083 o el 80, los seguros en el 8084 o el 443. Cambia el path con client.ws_set_options(path=...) si tu broker o tu proxy usan otro.
¿paho-mqtt respeta HTTPS_PROXY o HTTP_PROXY?
No. paho solo lee una variable de entorno llamada mqtt_proxy (con una URL http:// o socks://) y solo si PySocks está instalado. En los demás casos, configura el proxy explícitamente con client.proxy_set(proxy_type=socks.HTTP, proxy_addr=..., proxy_port=...).
¿Qué significa WebSocket handshake error, connection not upgraded?
El servidor respondió a la petición HTTP de upgrade pero no cambió a WebSocket. Normalmente el path es incorrecto (una página 404), has llegado a un servidor web o proxy normal en lugar del listener WebSocket del broker, o un proxy eliminó la cabecera Upgrade.


