Ir al contenido principal
OpenAI

4 de mayo de 2026

Ingeniería

Cómo OpenAI ofrece IA de voz de baja latencia a escala

Por Yi Zhang y William McDonald, miembros del personal técnico

La IA de voz solo resulta natural si la conversación avanza al ritmo del habla. Cuando la red se interpone, la gente lo percibe enseguida en forma de pausas incómodas, interrupciones cortadas o respuestas tardías al barge-in (la interrupción del usuario). Esto importa para ChatGPT Voz, para los desarrolladores que crean con la Realtime API, para los agentes que trabajan en flujos interactivos y para los modelos que necesitan procesar audio mientras el usuario sigue hablando.

A la escala de OpenAI, eso se traduce en tres requisitos concretos:

  • Alcance global para más de 900 millones de usuarios activos semanales
  • Establecimiento rápido de la conexión para que un usuario pueda empezar a hablar en cuanto comience una sesión
  • Tiempo de ida y vuelta de medios bajo y estable, con poco jitter y pérdida de paquetes, para que los turnos de palabra sean ágiles

El equipo de OpenAI responsable de las interacciones de IA en tiempo real rediseñó recientemente nuestra pila de WebRTC para abordar tres restricciones que empezaban a entrar en conflicto a escala: la terminación de medios de un puerto por sesión no encaja bien con la infraestructura de OpenAI, las sesiones con estado de ICE (Interactive Connectivity Establishment) y DTLS (Datagram Transport Layer Security) necesitan un titular estable, y el enrutamiento global debe mantener baja la latencia del primer salto. En esta publicación, explicamos la arquitectura dividida en relay y transceptor que construimos para preservar el comportamiento estándar de WebRTC para los clientes mientras cambiamos la forma en que se enrutan los paquetes dentro de la infraestructura de OpenAI.

WebRTC nos permite crear productos de IA en tiempo real

WebRTC es un estándar abierto para enviar audio, vídeo y datos de baja latencia entre navegadores, aplicaciones móviles y servidores. Suele asociarse con llamadas entre pares, pero también es una base práctica para sistemas en tiempo real de cliente a servidor porque estandariza las partes difíciles de los medios interactivos: ICE para el establecimiento de conectividad y el recorrido de NAT (Network Address Translation), DTLS y SRTP (Secure Real-time Transport Protocol) para el transporte cifrado, la negociación de códecs para comprimir y decodificar audio, RTCP (Real-time Transport Control Protocol) para el control de calidad y funciones del lado del cliente como la cancelación de eco y el búfer de jitter.

Esa estandarización importa para los productos de IA. Sin WebRTC, cada cliente necesitaría una respuesta distinta para establecer conectividad a través de NAT, cifrar medios, negociar códecs (los codificadores-decodificadores seleccionados para la transmisión y descompresión) y adaptarse a las condiciones cambiantes de la red. Con WebRTC, podemos construir sobre una pila de protocolos ya implementada en navegadores y plataformas móviles, y centrar nuestro trabajo en la infraestructura que conecta los medios en tiempo real con los modelos.

También nos apoyamos en el propio ecosistema de WebRTC, incluidas implementaciones maduras de código abierto y el trabajo de estandarización que mantiene interoperables a navegadores, aplicaciones móviles y servidores. El trabajo fundamental de Justin Uberti (uno de los arquitectos originales de WebRTC) y Sean DuBois (creador y responsable de Pion) hizo posible que equipos como el nuestro pudieran construir sobre una infraestructura de medios probada en producción en lugar de reinventar el transporte de bajo nivel, el cifrado y el control de congestión. Tenemos la suerte de que tanto Justin como Sean sean ahora compañeros aquí en OpenAI, ayudando a orientar cómo acercamos WebRTC y la IA en tiempo real.

Para la IA, la propiedad más importante es que el audio llegue como un flujo continuo. Un agente de voz puede empezar a transcribir, razonar, invocar herramientas o generar voz mientras el usuario sigue hablando, en lugar de esperar a que termine la subida completa. Esa es la diferencia entre un sistema que resulta conversacional y otro que funciona como un push-to-talk.

Elegir una arquitectura de medios

Una vez que elegimos WebRTC, la siguiente pregunta fue dónde terminarlo (dónde aceptaríamos y asumiríamos la conexión WebRTC —por ejemplo, en el borde—) y cómo conectar esas sesiones con el backend de inferencia. La terminación importa porque determina cómo gestionamos el estado de la sesión en tiempo real, el transporte de medios, el enrutamiento, la latencia y el aislamiento de fallos.

Opción 1: el enfoque SFU incluye la IA como participante de WebRTC

Una SFU, o unidad de reenvío selectivo, es un servidor de medios que recibe un flujo WebRTC de cada participante y reenvía selectivamente los flujos a los demás. En este modelo, la SFU termina una conexión WebRTC independiente para cada participante, y la IA se une como otro participante de la sesión. Puede encajar bien en productos que son inherentemente multiparticipante, como llamadas de grupo, aulas o reuniones colaborativas. Mantiene en un solo lugar los códecs de audio, los mensajes RTCP, los canales de datos, la grabación y la política por flujo.1

Incluso en productos de cliente a IA, una SFU suele ser el punto de partida predeterminado porque permite a los equipos reutilizar un sistema ya probado para señalización, enrutamiento de medios, grabación, observabilidad y futuras ampliaciones como la transferencia a un humano o la incorporación de más participantes.

Opción 2: el enfoque de transceptor termina WebRTC en el borde y lo convierte en un protocolo backend

Nuestra carga de trabajo es distinta. La mayoría de las sesiones son 1:1: un usuario hablando con un modelo, o una aplicación hablando con un agente en tiempo real, con sensibilidad a la latencia en cada turno. Para esa forma de tráfico, elegimos un modelo de transceptor: un servicio perimetral de WebRTC termina la conexión del cliente y luego convierte los medios y eventos en protocolos internos más simples para la inferencia del modelo, la transcripción, la generación de voz, el uso de herramientas y la orquestación.

En este diseño, el transceptor es el único servicio que posee el estado de la sesión WebRTC, incluidas las comprobaciones de conectividad ICE, el protocolo de enlace DTLS, las claves de cifrado SRTP y el ciclo de vida de la sesión. Aquí, «terminación» significa que el transceptor es el punto de acceso que completa esos protocolos de enlace y cifra o descifra los medios. Mantener ese estado en un solo lugar facilitó el análisis de la titularidad de la sesión y permitió que los servicios backend escalaran como servicios normales en lugar de actuar ellos mismos como pares WebRTC.

El problema central del despliegue: WebRTC se encuentra con Kubernetes

Tras elegir el modelo de transceptor, nuestra primera implementación fue un único servicio en Go basado en Pion que gestionaba tanto la señalización como la terminación de medios. Impulsa ChatGPT Voz, el punto de acceso WebRTC de la Realtime API y varios proyectos de investigación.

Desde el punto de vista operativo, el servicio de transceptor hace dos trabajos:

  • Señalización: negociación SDP, selección de códecs, credenciales ICE y configuración de la sesión
  • Medios: terminación de conexiones WebRTC descendentes y mantenimiento de conexiones ascendentes a servicios backend para inferencia y orquestación

Queríamos que el servicio se ejecutara como el resto de nuestra infraestructura: en Kubernetes, donde las cargas de trabajo pueden ampliarse y reducirse, y moverse entre hosts según cambia la demanda. Pero el modelo convencional de WebRTC de un puerto por sesión encaja mal en ese entorno, porque depende de grandes rangos de puertos UDP públicos que son difíciles de exponer, proteger y conservar a medida que se añaden, eliminan o reprograman pods.2

Agotamiento de puertos

El primer problema era el propio modelo de un puerto por sesión. Con una alta concurrencia, eso implica exponer y gestionar rangos muy amplios de puertos UDP.

  • Los equilibradores de carga en la nube y los servicios de Kubernetes no están diseñados en torno a decenas de miles de puertos UDP públicos por servicio. Cada rango adicional añade complejidad operativa en la configuración del equilibrador, las comprobaciones de estado, la política de cortafuegos y la seguridad de los despliegues.3
  • Los grandes rangos de puertos UDP son difíciles de proteger porque amplían la superficie accesible desde el exterior y complican la auditoría de la política de red.
  • Además, encajan mal con el autoescalado. Los pods se añaden, eliminan y reprograman constantemente en Kubernetes. Exigir que cada pod reserve y publique un amplio rango de puertos estables vuelve frágil esa elasticidad.4

Por eso muchos sistemas WebRTC avanzan hacia un único puerto UDP por servidor, con demultiplexación a nivel de aplicación detrás de ese puerto.5

Persistencia del estado

Los diseños de un solo puerto por servidor resuelven el número de puertos, pero introducen un segundo problema: preservar la propiedad de cada sesión en toda una flota.

ICE y DTLS son protocolos con estado. El proceso que creó una sesión necesita seguir recibiendo los paquetes de esa sesión para poder validar las comprobaciones de conectividad, completar el protocolo de enlace DTLS, descifrar SRTP y procesar cambios posteriores de la sesión como los reinicios de ICE. Si los paquetes de la misma sesión llegan a un proceso distinto, el establecimiento puede fallar o los medios pueden romperse.

Eso nos dio un objetivo concreto: exponer a la internet pública una superficie UDP pequeña y fija, y al mismo tiempo enrutar cada paquete al transceptor que posee la sesión WebRTC correspondiente.

Comparación de arquitecturas de medios WebRTC

Evaluamos varias formas de conseguirlo, incluido TURN (Traversal Using Relays around NAT), donde un relay perimetral termina las asignaciones de cliente y reenvía el tráfico en su nombre.2

Enfoque

Ventajas

Inconvenientes

IP:puerto único por sesión (también conocido como UDP directo nativo)

Ruta directa de medios del cliente al servidor

Sin capa de reenvío en la ruta de datos

Requiere un puerto UDP público por sesión

Los rangos de puertos grandes son difíciles de exponer y proteger

Encaja mal con Kubernetes y los equilibradores de carga en la nube

IP:puerto único por servidor

Huella UDP pública mucho menor que la exposición por sesión

Un socket compartido por servidor puede demultiplexar muchas sesiones

Funciona limpiamente en un único host, pero no por sí solo en una flota compartida con equilibrio de carga

La demultiplexación de sesiones en un único host solo ayuda después de que un paquete llega a ese host; en una flota con equilibrio de carga, el primer paquete puede seguir llegando a la instancia equivocada, así que sigue haciendo falta una forma determinista de dirigir cada sesión al proceso que la posee


Relay TURN (con terminación de protocolo)

Los clientes solo necesitan llegar a la dirección y el puerto del relay TURN

Puede centralizar la política en el borde

Las asignaciones TURN añaden viajes de ida y vuelta durante la configuración

Sigue siendo difícil mover o recuperar asignaciones entre servidores TURN

Reenviador sin estado + terminador con estado (relay + transceptor de OpenAI)

Huella UDP pública pequeña

El transceptor sigue siendo propietario de toda la sesión WebRTC

Añade un salto de reenvío antes de que los medios lleguen al transceptor propietario

Requiere coordinación personalizada entre relay y transceptor

Visión general de la arquitectura: relay + transceptor

La arquitectura que implementamos separa el enrutamiento de paquetes de la terminación del protocolo. La señalización sigue llegando al transceptor para la configuración de la sesión, mientras que los medios entran primero por el relay. El relay es una capa ligera de reenvío UDP con una pequeña huella pública, y el transceptor es el punto de acceso WebRTC con estado que está detrás.

Relay reenvía paquetes al transceptor sin mantener estado

El relay no descifra medios, no ejecuta máquinas de estado ICE ni participa en la negociación de códecs. Lee suficientes metadatos de los paquetes para elegir un destino y luego reenvía el paquete al transceptor que posee la sesión. El transceptor sigue viendo un flujo WebRTC normal y sigue poseyendo todo el estado del protocolo. Desde la perspectiva del cliente, no cambia nada de la sesión WebRTC.

Enrutamiento basado en credenciales ICE

El enrutamiento del primer paquete es el paso clave de esta configuración. Un relay tiene que enrutar el primer paquete de un cliente antes de que exista ninguna sesión en la propia ruta del paquete, en lugar de detenerse en un servicio externo de búsqueda.

Toda sesión WebRTC ya incluye un mecanismo de enrutamiento nativo del protocolo: el fragmento de nombre de usuario de ICE, o ufrag, un identificador corto intercambiado durante la configuración de la sesión y repetido en las comprobaciones de conectividad STUN. Generamos el ufrag del lado del servidor para que contenga justo los metadatos de enrutamiento necesarios para que el relay pueda inferir el clúster de destino y el transceptor propietario.

El diagrama de secuencia muestra cómo se establece la conexión

Durante la señalización, el transceptor asigna el estado de la sesión y devuelve en la respuesta SDP una VIP de relay compartida y un puerto UDP. Una VIP es una dirección IP virtual que está delante de la flota de relay; combinada con el puerto, da al cliente un único destino estable, como `203.0.113.10:3478`, aunque haya muchas instancias de relay detrás. El primer paquete de la ruta de medios del cliente suele ser una solicitud de enlace STUN (Session Traversal Utilities for NAT), que ICE usa para verificar que los paquetes pueden llegar a la dirección anunciada.

Relay analiza solo lo necesario de ese primer paquete STUN para leer el ufrag del servidor, descodificar la pista de enrutamiento y reenviar el paquete al transceptor propietario. Cada transceptor escucha en un socket UDP compartido, es decir, un punto de acceso del sistema operativo vinculado a una IP:puerto interna, no un socket por sesión. Después de que el relay crea una sesión desde la IP:puerto de origen del cliente hasta ese destino del transceptor, los paquetes DTLS, RTP y RTCP posteriores fluyen dentro de la sesión sin volver a descodificar el ufrag.

La sesión del relay es deliberadamente mínima y consiste solo en una sesión en memoria para guiar el reenvío de paquetes, junto con los contadores necesarios para la monitorización y temporizadores para el vencimiento y la limpieza de la sesión. Esta decisión de diseño mantiene el enrutamiento de paquetes directamente en la propia ruta del paquete. Si un relay se reinicia y pierde la sesión, el siguiente paquete STUN vuelve a construir la sesión a partir de la pista de enrutamiento del ufrag. Para hacerlo aún más fiable, se emplea una caché de Redis para guardar la asignación de <IP + puerto del cliente, IP + puerto del transceptor> una vez establecida la ruta, de modo que pueda recuperarse mucho antes, antes de que llegue el siguiente paquete STUN.

Global Relay y señalización dirigida geográficamente

Una vez que reducimos la superficie UDP pública a un pequeño número de direcciones y puertos estables, pudimos desplegar el mismo patrón de relay a escala global. Global Relay es nuestra flota de puntos de entrada relay distribuidos geográficamente, que implementan el mismo comportamiento de reenvío de paquetes.

Una amplia entrada geográfica acorta el primer salto del cliente a OpenAI porque un paquete puede entrar en nuestra red por un relay cercano al usuario, tanto geográficamente como en topología de red, en lugar de cruzar primero la internet pública hasta una región lejana. En términos prácticos, eso significa menor latencia, menos jitter y menos ráfagas evitables de pérdida de paquetes antes de que el tráfico llegue a nuestra red troncal.6

La capa Global Relay recibe paquetes del cliente y los reenvía al clúster de transceptores

Usamos la geolocalización y el direccionamiento por proximidad de Cloudflare para la señalización, de modo que la solicitud HTTP o WebSocket inicial llegue a un clúster de transceptores cercano. El contexto de la solicitud determina la ubicación de la sesión y qué punto de entrada de Global Relay se anuncia al cliente. La respuesta SDP proporciona la dirección de Global Relay, mientras que el ufrag contiene información suficiente para que Global Relay enrute los medios al clúster designado y para que el relay los enrute al transceptor de destino.

En conjunto, la señalización dirigida geográficamente y Global Relay sitúan tanto la configuración como los medios en una ruta de entrada cercana, al tiempo que mantienen la sesión anclada a un único transceptor. Eso reduce el tiempo de ida y vuelta de la señalización y de la primera comprobación de conectividad ICE, lo que acorta directamente el tiempo que un usuario espera antes de poder empezar a hablar.

Implementación y rendimiento del relay

Escribimos el servicio relay en Go y mantuvimos la implementación deliberadamente acotada. En Linux, la pila de red del kernel recibe paquetes UDP desde la interfaz de red de la máquina y los entrega a un socket, el punto de acceso del sistema operativo que un proceso lee tras vincular una IP:puerto. Relay se ejecuta en espacio de usuario, así que un proceso normal de Go lee las cabeceras de los paquetes desde ese socket, actualiza una pequeña cantidad de estado de flujo y reenvía paquetes sin terminar WebRTC. No necesitamos ningún framework de bypass del kernel, que permitiría a un proceso en espacio de usuario consultar directamente las colas de red para tasas de paquetes más altas, pero también añadiría complejidad operativa.

Decisiones clave de diseño:

  • Sin terminación del protocolo: Relay analiza solo las cabeceras STUN/ufrag; usa estado en caché para DTLS, RTP y RTCP posteriores, manteniendo los paquetes opacos.
  • Estado efímero: Mantiene un pequeño mapa en memoria, con tiempos de espera cortos, de dirección del cliente a destino del transceptor para el estado de flujo y la observabilidad.
  • Escalabilidad horizontal: Varias instancias de relay se ejecutan en paralelo detrás de un equilibrador de carga. El estado no es un estado duro de WebRTC, por lo que los reinicios provocan mínimas caídas de tráfico y una rápida recuperación del flujo.

Medidas de eficiencia:

  • SO_REUSEPORT es una opción de socket de Linux que permite que varios workers de relay en la misma máquina vinculen el mismo puerto UDP. El kernel distribuye entonces los paquetes entrantes entre esos workers, lo que evita un cuello de botella en un único bucle de lectura.
  • runtime.LockOSThread fija cada goroutine que lee UDP a un hilo específico del sistema operativo. Combinado con SO_REUSEPORT, eso tiende a mantener en el mismo núcleo de CPU los paquetes del mismo flujo (la IP:puerto de origen y destino más el protocolo), mejorando la localidad de caché y reduciendo los cambios de contexto.
  • Los búferes preasignados y la copia mínima mantienen bajos los costes de análisis y asignación para evitar la recolección de basura en Go.

Esta implementación gestionó nuestro tráfico global de medios en tiempo real con una huella de relay relativamente pequeña, así que mantuvimos el diseño más simple en lugar de asumir una vía de bypass del kernel.

Resultados y aprendizajes

Esta arquitectura nos permite ejecutar medios WebRTC en Kubernetes sin exponer miles de puertos UDP. Eso importa porque una superficie UDP más pequeña y fija es más fácil de proteger y equilibrar, y permite que la infraestructura escale sin reservar grandes rangos de puertos públicos. Con un mejor soporte por parte de Kubernetes y más seguridad gracias a una superficie más reducida, este diseño también preserva el comportamiento estándar de WebRTC para los clientes y confirma que un diseño sin SFU era la opción predeterminada correcta para nuestra carga de trabajo. La mayoría de nuestras sesiones son punto a punto, sensibles a la latencia y más fáciles de escalar cuando los servicios de inferencia no necesitan comportarse como pares WebRTC.

La lección más amplia es que el mejor lugar para añadir complejidad está en una capa fina de enrutamiento, no en cada servicio backend ni en un comportamiento personalizado del cliente. Codificar metadatos de enrutamiento en un campo nativo del protocolo nos dio un enrutamiento determinista del primer paquete, una pequeña huella UDP pública y suficiente flexibilidad para situar la entrada cerca de los usuarios de todo el mundo.

Algunas decisiones fueron especialmente importantes:

  • Preservar la semántica del protocolo en el borde. Los clientes siguen usando WebRTC estándar, lo que mantiene intacta la interoperabilidad con navegadores y móviles.
  • Mantener los estados de sesión complejos en un solo lugar. El transceptor es propietario de ICE, DTLS, SRTP y del ciclo de vida de la sesión; relay solo reenvía paquetes.
  • Enrutar basándose en información ya presente en la configuración. El ufrag de ICE nos dio un mecanismo de enrutamiento para el primer paquete sin añadir una dependencia de consulta en la ruta crítica.
  • Optimizar para el caso más común antes de recurrir al bypass del kernel. Una implementación acotada en Go con un uso cuidadoso de SO_REUSEPORT, fijación de hilos y análisis de baja asignación fue suficiente para nuestra carga de trabajo.

La IA de voz en tiempo real solo funciona cuando la infraestructura hace que la latencia se sienta invisible. Para nosotros, eso significó cambiar la forma de nuestro despliegue de WebRTC sin cambiar lo que los clientes esperan del propio WebRTC.