
WebSockets en Responses API: agentes OpenAI más rápidos
TL;DR: OpenAI ha añadido WebSocket Mode a la Responses API, una conexión persistente contra wss://api.openai.com/v1/responses que reduce hasta un 40% la latencia en agentes con muchas tool calls. En modelos como GPT-5.3-Codex-Spark, los developers han pasado de 65 a más de 1000 tokens por segundo sostenidos. En este artículo verás cuándo aplicarlo, cómo conectarte desde Python y qué cambia respecto al HTTP de toda la vida.
El problema: la API se convirtió en el cuello de botella
Si has trabajado con un agente que encadena tool calls, conoces la sensación: el modelo es rapidísimo, tu código está optimizado, y aun así cada turno se siente lento. La razón no está en la inferencia, está en el transporte.
Cada llamada HTTP a /v1/responses abre una conexión TLS, atraviesa el balanceador, levanta un worker y vuelve a cerrar todo. Cuando un agente hace 20, 30 o 100 tool calls en una sesión, ese coste fijo se acumula. En la documentación oficial, OpenAI mide hasta un 40% de mejora end-to-end en rollouts con 20 o más tool calls al cambiar a WebSockets. Codex movió la mayoría de su tráfico a este modo y los modelos en Cursor llegaron a ser un 30% más rápidos.
El cambio importa porque los modelos están acelerando muy por encima de la red. GPT-5.3-Codex-Spark alcanza 1000 TPS sostenidos y picos de 4000 TPS. Si el modelo escupe la respuesta en 200ms pero el round-trip HTTP suma 400ms más por turno, el agente percibido es lento aunque el LLM no lo sea.
¿Qué es WebSocket Mode en la Responses API?
WebSocket Mode es un transporte alternativo para la Responses API que mantiene una conexión persistente con OpenAI y permite enviar turnos incrementales sin renegociar TLS ni reenviar el historial completo. No es una API nueva, son los mismos eventos (response.create, response.cancel, eventos de streaming) viajando por un canal abierto en lugar de por requests HTTP independientes.
La idea clave: en cada turno solo envías los nuevos input items (output de la última tool, mensaje del usuario) y un previous_response_id. OpenAI conserva el contexto del lado servidor y devuelve los eventos de la siguiente respuesta por el mismo socket.
Diferencias frente al modo HTTP
| Aspecto | HTTP estándar | WebSocket Mode |
|---|---|---|
| Conexión | Una por turno | Persistente, hasta 60 min |
| Latencia por tool call | Alta (TLS + DNS por turno) | Baja (canal ya abierto) |
| Concurrencia | Múltiples requests en paralelo | Una respuesta en vuelo por socket |
| Reintentos en error | Trivial, idempotente | Reconectar y continuar con previous_response_id |
| Caso ideal | Chats puntuales, jobs en lote | Agentes con 20+ tool calls |
La regla práctica es simple: si tu agente hace pocos turnos largos, HTTP está bien. Si encadena muchos turnos cortos con tools, WebSockets paga su complejidad.
Implementación paso a paso en Python
Vamos a conectar un cliente Python al endpoint y enviar un primer turno. Asumo que tienes OPENAI_API_KEY en variables de entorno y la librería websocket-client instalada.
1. Abrir la conexión autenticada
Construyes una conexión WebSocket contra el endpoint de la Responses API pasando tu API key en el header.
# Conexión persistente a la Responses API por WebSocket
import os
import json
from websocket import create_connection
ws = create_connection(
"wss://api.openai.com/v1/responses",
header=[f"Authorization: Bearer {os.environ['OPENAI_API_KEY']}"],
)
Output esperado: el socket queda abierto, sin tráfico todavía. Si la API key es inválida, el handshake falla con un cierre inmediato del socket.
2. Enviar el primer turno con response.create
El payload es el mismo que enviarías por HTTP, pero envuelto como evento response.create. Los campos stream y background no aplican aquí.
# Primer turno: input inicial y herramientas disponibles
ws.send(json.dumps({
"type": "response.create",
"model": "gpt-5.4",
"input": [
{"role": "user", "content": "Resume el último deploy de mi servicio"}
],
"tools": [{"type": "function", "name": "get_deploy_log", "parameters": {}}],
}))
3. Consumir eventos del socket
El servidor devuelve un stream de eventos JSON: deltas de tokens, llamadas a tools y un response.completed final con el response.id que necesitarás para el siguiente turno.
# Lectura del stream hasta cierre de la respuesta
response_id = None
while True:
event = json.loads(ws.recv())
if event["type"] == "response.completed":
response_id = event["response"]["id"]
break
if event["type"] == "response.output_item.added":
# Aquí detectarías una tool call y la ejecutarías localmente
pass
4. Continuar el agente con previous_response_id
Para el segundo turno solo envías el output de la tool y el ID de la respuesta anterior. OpenAI reconstruye el contexto sin que tú reenvíes el historial.
# Turno siguiente: solo lo nuevo + referencia al turno anterior
ws.send(json.dumps({
"type": "response.create",
"model": "gpt-5.4",
"previous_response_id": response_id,
"input": [
{"type": "function_call_output", "call_id": "...", "output": "deploy ok"}
],
}))
Este patrón es lo que reduce la latencia: cada turno viaja por la misma conexión y carga solo el delta. En un agente con 30 tool calls, eso significa 30 round-trips evitados.
Caso real: cuándo lo aplicarías de verdad
En escenarios reales, los candidatos típicos son agentes de soporte que consultan APIs internas en cada turno, copilotos de código que ejecutan tools de búsqueda y edición sobre el repositorio, y bots de automatización que navegan flujos largos paso a paso. Si estás construyendo algo parecido a Codex o Cursor, probablemente notarás la diferencia.
Para integrar tools externas en un agente, lo más limpio es separar el cliente WebSocket del registro de funciones, igual que aplicarías el principio de separación de responsabilidades en cualquier microservicio. El socket gestiona transporte, el registro decide qué función Python ejecutar al recibir una tool call.
Para agentes que además tiran de RAG sobre documentos, el patrón se combina bien con un pipeline de procesamiento de PDFs con LangChain: la búsqueda semántica vive en su propio servicio, y el agente la consume como una tool más sobre la conexión persistente.
En Producción
Pasar un POC a producción con WebSockets exige resolver tres cosas que en HTTP venían gratis.
1. Reconexiones. La conexión está limitada a 60 minutos. Si tu agente puede vivir más, necesitas reconectar y continuar con previous_response_id. Si guardaste la respuesta con store=true es directo. Con store=false o Zero Data Retention, tienes que reenviar el contexto desde tu lado.
2. Concurrencia. Cada socket procesa una respuesta en vuelo. Para correr varios agentes en paralelo, abres varias conexiones. No intentes multiplexar turnos en la misma conexión, no está soportado y los eventos se mezclarán.
3. Coste. El precio por token es el mismo que en HTTP. Lo que ahorras es latencia, no factura. Aun así, una sesión más rápida suele consumir menos tokens en retries y prompts repetidos. Si vienes de medir todo en HTTP, refactoriza tus métricas para registrar tokens por sesión y latencia por turno por separado.
Para infraestructura, ten en cuenta que mantener miles de WebSockets vivos en un backend Python no es trivial. Si vienes de un stack tradicional, los mismos principios que aplicas con WebSockets en Python con Flask aplican aquí: workers asíncronos, gestión de heartbeats y un timeout claro por sesión. Si manejas alta carga, evalúa orquestar los sockets desde un servicio dedicado, no desde tu API principal de negocio, igual que harías con cualquier microservicio en Node.js y Express.
Errores comunes y cómo depurarlos
- Error: el handshake falla con 401. Causa: la API key no llega en el header. Solución: revisa que pasas
Authorization: Bearer ...en el header del WebSocket, no como query param. - Error: response.create devuelve "previous_response_id not found". Causa: usaste
store=falseen el turno anterior y el contexto no se persistió. Solución: o activasstore=trueo reenvías el historial completo en el siguienteresponse.create. - Error: el socket se cierra a los 60 minutos sin aviso. Causa: límite de duración de conexión. Solución: implementa una capa de reconexión que detecte el cierre, abra un socket nuevo y reanude con
previous_response_id. - Error: eventos mezclados entre dos respuestas. Causa: enviaste un segundo
response.createantes de recibirresponse.completed. Solución: serializa los turnos en cliente o abre una segunda conexión para el segundo agente.
Cuándo NO usar WebSocket Mode
No todo agente gana cambiando de transporte. Si tu caso es un asistente de un solo turno, un job batch nocturno o un endpoint que recibe un prompt y devuelve una respuesta, HTTP es más simple, más fácil de depurar y soporta paralelismo trivial. WebSockets pagan su complejidad cuando hay muchos turnos cortos y la latencia percibida importa.
Si además de OpenAI estás usando Claude Opus 4.7 en tu flujo, ten en cuenta que cada proveedor optimiza su loop agentic distinto. Anthropic no tiene un equivalente directo a este WebSocket Mode todavía, así que si tu agente es multi-modelo, vas a vivir con dos transportes diferentes durante un tiempo.
Preguntas frecuentes
¿Cuándo conviene usar WebSocket Mode en lugar de HTTP en la Responses API?
Cuando tu agente encadena 20 o más tool calls por sesión o cuando la latencia entre turnos importa más que la simplicidad. Para chats de un solo turno o tareas de baja frecuencia, HTTP sigue siendo más simple y cubre el caso sin cambios de stack.
¿Necesito un SDK especial o puedo usar el cliente WebSocket estándar?
Cualquier cliente WebSocket compatible con cabeceras de autenticación funciona. En Python suele bastar con websocket-client o websockets, autenticando con el header Authorization: Bearer y enviando eventos response.create en JSON.
¿Qué pasa si la conexión se corta a mitad de un agente largo?
La conexión está limitada a 60 minutos y solo permite una respuesta en vuelo. Si se corta y guardaste la respuesta con store=true, puedes reconectar y continuar usando previous_response_id. Con store=false o Zero Data Retention, tienes que reenviar el contexto completo.
Conclusión
WebSocket Mode no es una API nueva, es el mismo contrato de la Responses API moviéndose por un canal persistente. La ganancia real aparece cuando tu agente encadena muchos turnos con tools, no cuando hace una sola pregunta. Si estás construyendo algo agentic con OpenAI y notas que la latencia se acumula turno a turno, este es el cambio que probablemente te dé el mejor retorno por hora invertida.
La parte que más me ha sorprendido al revisarlo es lo poco que cambia el código de aplicación: el registro de tools, el ciclo de eventos y el manejo de contexto siguen casi iguales. Lo que cambia es la capa de transporte y, con ella, la percepción de velocidad del usuario final.
¿Has migrado ya algún agente a WebSocket Mode o lo estás evaluando? Cuéntame en Twitter @sergiomarquezp_ qué tal te ha ido con la latencia. En el próximo post entraré en cómo orquestar varios agentes en paralelo cuando cada uno necesita su propia conexión persistente sin saturar el backend.


