Hoe OpenAI voice-AI met lage latentie op schaal levert
Door Yi Zhang en William McDonald, leden van de technische staf
Voice AI voelt alleen natuurlijk aan als een gesprek zich met de snelheid van spraak voortbeweegt. Als het netwerk in de weg zit, horen mensen dat meteen aan ongemakkelijke stiltes, abrupt afgebroken reacties of vertraagde onderbrekingen. Dat is belangrijk voor ChatGPT Voice, voor ontwikkelaars die bouwen met de Realtime API, voor agents die werken in interactieve workflows en voor modellen die audio moeten verwerken terwijl een gebruiker nog praat.
Op de schaal van OpenAI vertaalt zich dat in drie concrete vereisten:
- Wereldwijd bereik voor meer dan 900 miljoen wekelijks actieve gebruikers
- Een verbinding die snel tot stand komt, zodat gebruikers kunnen beginnen met praten zodra een sessie start
- Lage en stabiele round-trip-tijd voor media, met weinig jitter en pakketverlies, zodat beurtwisseling vlot aanvoelt
Het team bij OpenAI dat verantwoordelijk is voor realtime AI-interacties heeft onlangs onze WebRTC-stack opnieuw ontworpen om drie beperkingen aan te pakken die op grote schaal steeds meer begonnen te wringen: mediaterminatie met één poort per sessie past niet goed binnen de infrastructuur van OpenAI, ICE- (Interactive Connectivity Establishment) en DTLS-sessies (Datagram Transport Layer Security) met statusinformatie hebben een stabiele beheerstructuur nodig, en wereldwijde routering moet de latentie van de eerste hop laag houden. In dit artikel bespreken we de gesplitste relay-plus-transceiver-architectuur die we hebben gebouwd om standaard WebRTC-gedrag voor clients te behouden, terwijl we de routering van pakketten binnen de infrastructuur van OpenAI aanpassen.
WebRTC is een open standaard voor het verzenden van audio, video en data met lage latentie tussen browsers, mobiele apps en servers. Het wordt vaak geassocieerd met peer-to-peergesprekken, maar het is ook een praktische basis voor realtime systemen van client naar server, omdat het de lastige onderdelen van interactieve media standaardiseert: ICE voor het opzetten van connectiviteit en NAT- (Network Address Translation) traversal, DTLS en SRTP (Secure Real-time Transport Protocol) voor versleuteld transport, codec-onderhandeling voor het comprimeren en decoderen van audio, RTCP (Real-time Transport Control Protocol) voor kwaliteitscontrole en client-side functies zoals echo-onderdrukking en jitterbuffering.
Die standaardisatie is belangrijk voor AI-producten. Zonder WebRTC zou elke client een ander antwoord nodig hebben op de vraag hoe connectiviteit over NAT’s tot stand komt, media wordt versleuteld, codecs worden onderhandeld (de coder-decoders die voor transmissie en decompressie worden gekozen) en hoe er wordt omgegaan met veranderende netwerkomstandigheden. Met WebRTC kunnen we voortbouwen op een protocolstack die al is geïmplementeerd in browsers en mobiele platforms, zodat we ons eigen werk kunnen richten op de infrastructuur die realtime media met modellen verbindt.
We bouwen ook voort op het WebRTC-ecosysteem zelf, inclusief volwassen open-source-implementaties en het standaardiseringswerk dat browsers, mobiele apps en servers onderling compatibel houdt. Fundamenteel werk van Justin Uberti (een van de oorspronkelijke architecten van WebRTC) en Sean DuBois (maker en beheerder van Pion) maakte het mogelijk voor teams zoals het onze om voort te bouwen op beproefde media-infrastructuur in plaats van transport-, versleutelings- en congestion-controlgedrag op laag niveau opnieuw uit te vinden. We hebben het geluk dat Justin en Sean nu allebei collega’s zijn hier bij OpenAI en helpen sturen hoe we WebRTC en realtime AI dichter bij elkaar brengen.
Voor AI is de belangrijkste eigenschap dat audio als een continue stream binnenkomt. Een spraak-agent kan al beginnen met transcriberen, redeneren, tools aanroepen of spraak genereren terwijl de gebruiker nog praat, in plaats van te wachten op een volledige upload. Dat is het verschil tussen een systeem dat conversationeel aanvoelt en een systeem dat aanvoelt als push-to-talk.
Nadat we voor WebRTC hadden gekozen, was de volgende vraag waar we het zouden termineren (waar we de WebRTC-verbinding zouden accepteren en beheren, bijvoorbeeld aan de edge) en hoe we die sessies met de inferentiebackend zouden verbinden. Terminatie is belangrijk omdat die bepaalt hoe we realtime sessiestatus, mediatransport, routering, latentie en foutisolatie afhandelen.
Een SFU, of selective forwarding unit, is een mediaserver die van elke deelnemer één WebRTC-stream ontvangt en streams selectief doorstuurt naar de anderen. In dit model termineert de SFU voor elke deelnemer een afzonderlijke WebRTC-verbinding, en de AI neemt als nog een deelnemer deel aan de sessie. Dat kan goed passen bij producten die van nature multiparty zijn, zoals groepsgesprekken, klaslokalen of samenwerkingsvergaderingen. Het houdt audiocodecs, RTCP-berichten, datakanalen, opname en beleid per stream op één plek.1
Zelfs in client-naar-AI-producten is een SFU vaak het standaard uitgangspunt omdat teams daarmee één bewezen systeem kunnen hergebruiken voor signaling, mediaroutering, opname, observeerbaarheid en toekomstige uitbreidingen zoals overdracht naar een mens of het toevoegen van meer deelnemers.
Onze workload is anders. De meeste sessies zijn 1:1 (één gebruiker die met één model praat, of één applicatie die met één realtime agent praat) met latentiegevoeligheid bij elke beurt. Voor die verkeersvorm kozen we voor een transceiver-model: een WebRTC-edge-service termineert de clientverbinding en zet media en events daarna om in eenvoudigere interne protocollen voor modelinferentie, transcriptie, spraakgeneratie, toolgebruik en orkestratie.
In dit ontwerp is de transceiver de enige service die eigenaar is van de WebRTC-sessiestatus, inclusief ICE-connectiviteitscontroles, de DTLS-handshake, SRTP-versleutelingssleutels en de levenscyclus van de sessie. ‘Terminatie’ betekent hier dat de transceiver het endpoint is dat die handshakes voltooit en de media versleutelt of ontsleutelt. Door die status op één plek te houden, werd sessie-eigenaarschap eenvoudiger om over te redeneren, en konden backendservices schalen als gewone services in plaats van zelf als WebRTC-peers te moeten optreden.
Nadat we voor het transceivermodel hadden gekozen, was onze eerste implementatie één Go-service gebouwd op Pion die zowel signaling als mediaterminatie afhandelde. Die ondersteunt ChatGPT Voice, het WebRTC-endpoint van de Realtime API en een aantal onderzoeksprojecten.
Operationeel doet de transceiverservice twee dingen:
- Signaling: SDP-onderhandeling, codecselectie, ICE-inloggegevens en sessie-opzet
- Media: downstream WebRTC-verbindingen termineren en upstream verbindingen onderhouden met backendservices voor inferentie en orkestratie
We wilden dat de service draaide zoals de rest van onze infrastructuur: op Kubernetes, waar workloads op en af kunnen schalen en over hosts kunnen verplaatsen als de vraag verandert. Maar het conventionele WebRTC-model met één poort per sessie past slecht bij die omgeving, omdat het afhankelijk is van grote publieke UDP-poortbereiken die moeilijk bloot te stellen, te beveiligen en te behouden zijn wanneer pods worden toegevoegd, verwijderd of opnieuw ingepland.2
Het eerste probleem was het model met één poort per sessie zelf. Bij grote aantallen gelijktijdige sessies betekent dat dat zeer grote UDP-poortbereiken moeten worden blootgesteld en beheerd.
- Cloud-loadbalancers en Kubernetes-services zijn niet ontworpen rond tienduizenden publieke UDP-poorten per service. Elk extra bereik voegt operationele complexiteit toe in loadbalancerconfiguratie, health checks, firewallbeleid en veilige uitrol.3
- Grote UDP-poortbereiken zijn moeilijk te beveiligen omdat ze het extern bereikbare oppervlak vergroten en netwerkbeleid lastiger te auditen maken.
- Ze passen ook slecht bij autoscaling. In Kubernetes worden voortdurend pods toegevoegd, verwijderd en opnieuw ingepland. Als elke pod een groot stabiel poortbereik moet reserveren en adverteren, wordt die elasticiteit fragiel.4
Daarom gaan veel WebRTC-systemen richting één enkele UDP-poort per server, met demultiplexing op applicatieniveau achter die poort.5
Ontwerpen met één poort per server lossen het aantal poorten op, maar introduceren een tweede probleem: het behouden van eigenaarschap van elke sessie over een fleet heen.
ICE en DTLS zijn stateful protocollen. Het proces dat een sessie heeft aangemaakt, moet de pakketten van die sessie blijven ontvangen zodat het connectiviteitscontroles kan valideren, de DTLS-handshake kan voltooien, SRTP kan ontsleutelen en latere sessiewijzigingen zoals ICE-restarts kan verwerken. Als pakketten voor dezelfde sessie bij een ander proces terechtkomen, kan de opzet mislukken of kan media breken.
Dat gaf ons een specifiek doel: een klein, vast UDP-oppervlak blootstellen aan het publieke internet en toch elk pakket routeren naar de transceiver die eigenaar is van de bijbehorende WebRTC-sessie.
We hebben verschillende manieren geëvalueerd om daar te komen, waaronder TURN (Traversal Using Relays around NAT), waarbij een edge-relay clientallocaties termineert en verkeer namens hen doorstuurt.2
Aanpak | Voordelen | Nadelen |
Unieke IP:poort per sessie (ook bekend als native directe UDP) | Direct mediapad van client naar server Geen doorstuellaag in het datapad | Vereist één publieke UDP-poort per sessie Grote poortbereiken zijn moeilijk bloot te stellen en te beveiligen Past slecht bij Kubernetes en cloud-loadbalancers |
Unieke IP:poort per server | Veel kleiner publiek UDP-oppervlak dan blootstelling per sessie Eén gedeelde socket per server kan veel sessies demultiplexen | Werkt netjes op één enkele host, maar niet op zichzelf over een gedeelde, load-balanced fleet Sessiedemultiplexing op één enkele host helpt pas nadat een pakket die host bereikt; over een load-balanced fleet kan het eerste pakket nog steeds op de verkeerde instantie terechtkomen, dus je hebt nog steeds een deterministische manier nodig om elke sessie te sturen naar het proces dat er eigenaar van is |
TURN-relay (protocolterminerend) | Clients hoeven alleen het adres en de poort van de TURN-relay te bereiken Kan beleid centraliseren aan de edge | TURN-allocaties voegen extra round trips toe aan de opzet Allocaties verplaatsen of herstellen over TURN-servers heen blijft lastig |
Stateless forwarder + stateful terminator (OpenAI’s relay + transceiver) | Klein publiek UDP-oppervlak Transceiver blijft eigenaar van de volledige WebRTC-sessie | Voegt één doorstuurhop toe voordat media de eigenaarstransceiver bereikt Vereist aangepaste coördinatie tussen relay en transceiver |
De architectuur die we hebben uitgerold, splitst pakketroutering van protocolterminatie. Signaling bereikt nog steeds de transceiver voor sessie-opzet, terwijl media eerst via de relay binnenkomt. De relay is een lichte UDP-doorstuellaag met een klein publiek oppervlak, en de transceiver is het stateful WebRTC-endpoint daarachter.
De relay ontsleutelt geen media, voert geen ICE-statusmachines uit en neemt niet deel aan codec-onderhandeling. Hij leest net genoeg pakketmetadata om een bestemming te kiezen en stuurt het pakket vervolgens door naar de transceiver die eigenaar is van de sessie. De transceiver ziet nog steeds een normale WebRTC-stroom en blijft eigenaar van alle protocolstatus. Vanuit het perspectief van de client verandert er niets aan de WebRTC-sessie.
Routering van het eerste pakket is de sleutelstap in deze opzet. Een relay moet het eerste pakket van een client routeren voordat er op het pakketpad zelf al een sessie bestaat, in plaats van te pauzeren voor een externe lookupservice.
Elke WebRTC-sessie heeft al een protocol-native routeringshaak: het ICE-gebruikersnaamfragment, of ufrag, een korte identifier die tijdens de sessie-opzet wordt uitgewisseld en wordt teruggekaatst in STUN-connectiviteitscontroles. We genereren de server-side ufrag zo dat die precies genoeg routeringsmetadata bevat zodat de relay de bestemmingscluster en de eigenaarstransceiver kan afleiden.
Tijdens signaling alloceert de transceiver sessiestatus en retourneert een gedeelde relay-VIP en UDP-poort in het SDP-antwoord. Een VIP is een virtueel IP-adres voor de relay-fleet; gecombineerd met de poort geeft het de client één stabiele bestemming, zoals `203.0.113.10:3478`, ook al zitten er veel relay-instanties achter. Het eerste pakket op het mediapad van de client is meestal een STUN-binding request, die ICE gebruikt om te verifiëren dat pakketten het geadverteerde adres kunnen bereiken.
De relay parseert net genoeg van dat eerste STUN-pakket om de server-ufrag te lezen, de routeringshint te decoderen en het pakket door te sturen naar de eigenaarstransceiver. Elke transceiver luistert op een gedeelde UDP-socket, wat betekent: één endpoint van het besturingssysteem gebonden aan een interne IP:poort, niet één socket per sessie. Nadat de relay een sessie heeft aangemaakt van de bron-IP:poort van de client naar die transceiverbestemming, stromen volgende DTLS-, RTP- en RTCP-pakketten binnen die sessie zonder de ufrag opnieuw te hoeven decoderen.
De sessie van de relay is bewust minimaal en bestaat alleen uit een in-memory sessie om pakketdoorsturing te informeren, samen met de nodige tellers voor monitoring en timers voor sessieverloop en opschoning. Deze ontwerpkeuze houdt pakketroutering direct op het pakketpad. Als een relay opnieuw start en de sessie verliest, bouwt het volgende STUN-pakket de sessie opnieuw op vanuit de routeringshint in de ufrag. Om het nog betrouwbaarder te maken, wordt een Redis-cache gebruikt om de mapping van <client-IP + poort, transceiver-IP + poort> vast te houden zodra de route is vastgesteld, zodat die veel eerder kan worden hersteld, voordat het volgende STUN-pakket arriveert.
Zodra we het publieke UDP-oppervlak hadden teruggebracht tot een klein aantal stabiele adressen en poorten, konden we hetzelfde relaypatroon wereldwijd uitrollen. Global Relay is onze fleet van geografisch gedistribueerde relay-ingresspunten die allemaal hetzelfde pakketdoorstuurgedrag implementeren.
Brede geografische ingress verkort de eerste hop van client naar OpenAI omdat een pakket ons netwerk kan binnenkomen via een relay dicht bij de gebruiker, zowel geografisch als in netwerktopologie, in plaats van eerst via het publieke internet een verre regio te moeten doorkruisen. In de praktijk betekent dat lagere latentie, minder jitter en minder vermijdbare verliespieken voordat verkeer onze backbone bereikt.6
We gebruiken Cloudflare geo- en proximity-steering voor signaling zodat het initiële HTTP- of WebSocket-verzoek een nabijgelegen transceivercluster bereikt. De context van het verzoek bepaalt de locatie van de sessie en welk Global Relay-ingresspunt aan de client wordt geadverteerd. Het SDP-antwoord geeft het Global Relay-adres, terwijl de ufrag voldoende informatie bevat voor Global Relay om media naar de aangewezen cluster te routeren en voor relay om naar de bestemmingstransceiver te routeren.
Samen zorgen geografisch gestuurde signaling en Global Relay ervoor dat zowel de opzet als media een nabijgelegen toegangspad volgen, terwijl de sessie verankerd blijft aan één transceiver. Dat verkleint de round-trip-tijd voor signaling en voor de eerste ICE-connectiviteitscontrole, wat direct verkort hoe lang een gebruiker moet wachten voordat spraak kan beginnen.
We schreven de relayservice in Go en hielden de implementatie bewust beperkt. Op Linux ontvangt de netwerkstack van de kernel UDP-pakketten van de netwerkinterface van de machine en levert die af aan een socket, het endpoint van het besturingssysteem dat een proces uitleest nadat het een IP:poort heeft gebonden. Relay draait in userspace, dus een regulier Go-proces leest pakketheaders van die socket, werkt een kleine hoeveelheid flowstatus bij en stuurt pakketten door zonder WebRTC te termineren. We hadden geen kernel-bypassframework nodig, waarmee een userspaceproces netwerkqueues direct kan pollen voor hogere pakketsnelheden maar dat ook operationele complexiteit toevoegt.
Belangrijke ontwerpkeuzes:
- Geen protocolterminatie: Relay parseert alleen STUN-headers/ufrag; voor daaropvolgende DTLS, RTP en RTCP gebruikt het gecachte status en blijven pakketten ondoorzichtig.
- Efemere status: Het onderhoudt een kleine in-memory map met korte time-out van clientadres naar transceiverbestemming voor flowstatus en observeerbaarheid.
- Horizontale schaalbaarheid: Meerdere relay-instanties draaien parallel achter een load balancer. De status is geen harde WebRTC-status, dus herstarts veroorzaken minimale verkeersuitval en snel herstel van flows.
Efficiëntiemaatregelen:
SO_REUSEPORTis een Linux-socketoptie waarmee meerdere relayworkers op dezelfde machine dezelfde UDP-poort kunnen binden. De kernel verdeelt binnenkomende pakketten dan over die workers, wat een bottleneck in één read-loop voorkomt.runtime.LockOSThreadpint elke UDP-lezende goroutine vast op een specifieke OS-thread. Gecombineerd metSO_REUSEPORTzorgt dat er meestal voor dat pakketten van dezelfde flow (bron- en bestemmings-IP:poort plus protocol) op dezelfde CPU-kern blijven, wat cachelokaliteit verbetert en context switching vermindert.- Vooraf toegewezen buffers en minimale copying houden parsing- en allocatie-overhead laag om garbage collection in Go te vermijden.
Deze implementatie verwerkte ons wereldwijde realtime mediaverkeer met een relatief kleine relay-footprint, dus hielden we vast aan het eenvoudigere ontwerp in plaats van een kernel-bypassroute te nemen.
Met deze architectuur kunnen we WebRTC-media in Kubernetes draaien zonder duizenden UDP-poorten bloot te stellen. Dat is belangrijk omdat een kleiner en vast UDP-oppervlak gemakkelijker te beveiligen en te load balancen is, en de infrastructuur daardoor kan schalen zonder grote publieke poortbereiken te reserveren. Met betere infrastructuurondersteuning van Kubernetes en meer beveiliging door het kleinere oppervlak behoudt dit ontwerp ook standaard WebRTC-gedrag voor clients en bevestigt het dat een ontwerp zonder SFU de juiste standaard was voor onze workload. De meeste van onze sessies zijn point-to-point, latentiegevoelig en gemakkelijker op te schalen wanneer inferentieservices zich niet als WebRTC-peers hoeven te gedragen.
De bredere les is dat je complexiteit het beste toevoegt in een dunne routeringslaag, niet in elke backendservice en niet in aangepast clientgedrag. Door routeringsmetadata te coderen in een protocol-native veld kregen we deterministische routering van het eerste pakket, een klein publiek UDP-oppervlak en genoeg flexibiliteit om ingress dicht bij gebruikers over de hele wereld te plaatsen.
Een paar keuzes waren bijzonder belangrijk:
- Behoud protocolsemantiek aan de edge. Clients spreken nog steeds standaard WebRTC, wat interoperabiliteit tussen browsers en mobiele apparaten intact houdt.
- Houd harde sessiestatus op één plek. De transceiver beheert ICE, DTLS, SRTP en de levenscyclus van de sessie; relay stuurt alleen pakketten door.
- Routeer op informatie die al aanwezig is bij de opzet. De ICE-ufrag gaf ons een routeringshaak voor het eerste pakket zonder een afhankelijkheid van een lookup op het hot path toe te voegen.
- Optimaliseer voor het veelvoorkomende geval voordat je naar kernel bypass grijpt. Een beperkte Go-implementatie met zorgvuldig gebruik van
SO_REUSEPORT, thread pinning en parsing met weinig allocaties was voldoende voor onze workload.
Realtime voice AI werkt alleen wanneer infrastructuur latentie onzichtbaar laat aanvoelen. Voor ons betekende dat de vorm van onze WebRTC-uitrol veranderen zonder te veranderen wat clients van WebRTC verwachten.


