Paano malawakang naghahatid ng low-latency voice AI ang OpenAI
Nina Yi Zhang at William McDonald, mga Member ng Technical Staff
Nagiging natural lang ang dating ng voice AI kapag kasimbilis ng pagsasalita ang takbo ng usapan. Kapag may problema ang network, alanganing mga paghinto ang agad na naririnig ng mga tao, putul-putol na interruption, o delayed na pagsasalita. Mahalaga ito para sa ChatGPT voice, para sa mga developer na nagbi-build gamit ang Realtime API, para sa mga agent sa mga interactive na workflow, at para sa mga model na kailangang i-process agad ang audio habang nagsasalita pa ang user.
Dahil sa lawak ng saklaw ng OpenAI, nangangahulugan ito ng tatlong kongkretong requirement:
- Global reach para sa mahigit 900 million na weekly active users
- Mabilis na connection setup para makakapagsalita agad ang user kapag nagsimula na ang session
- Mababa at stable na media round-trip time, na may mababang jitter at packet loss, para maging malinaw ang salitan ng pag-uusap
Kailan lang, binago ng team sa OpenAI na responsable sa real-time na mga AI interaction ang design ng WebRTC stack namin para ma-address ang tatlong constraint na nagsimulang maging di-compatible sa paglawak ng system: hindi gaanong fit sa infrastructure ng OpenAI ang one-port-per-session media termination, ang mga stateful session para sa ICE (Interactive Connectivity Establishment) at DTLS (Datagram Transport Layer Security) ay nangangailangan ng stable na ownership, at kailangang mapanatiling mababa ng global routing ang first-hop latency. Sa post na ito, ipapaliwanag namin ang ginawa naming architecture ng split na relay plus transceiver para ma-preserve ang standard WebRTC behavior para sa mga client habang binabago kung paano nira-route sa infrastructure ng OpenAI ang mga packet.
Ang WebRTC ay isang open standard para sa pagpapadala ng low-latency audio, video, at data sa pagitan ng mga browser, mobile app, at server. Madalas itong i-associate sa peer-to-peer calling, pero praktikal na pundasyon din ito para sa mga client-to-server real-time system dahil nagagawa nitong i-standardize ang mahihirap na part ng interactive media: ICE para sa pag-e-establish ng connectivity at NAT (Network Address Translation) traversal, DTLS at SRTP (Secure Real-time Transport Protocol) para sa encrypted transport, codec negotiation para sa pag-compress at pag-decode ng audio, RTCP (Real-time Transport Control Protocol) para sa quality control, at mga client-side feature na gaya ng echo cancellation at jitter buffering.
Mahalaga ang standardization na iyan para sa mga AI product. Kung wala ang WebRTC, kakailanganin ng bawat client ng magkakaibang sagot kung paano mag-establish ng connectivity sa mga NAT, mag-encrypt ng media, i-negotiate ang mga codec (ang mga coder-decoder na pinipili para sa transmission at decompression), at maka-adapt sa nagbabagong mga network condition. Sa WebRTC, makakapag-build kami sa protocol stack na naka-implement na sa mga browser at mobile platform, kaya maifo-focus namin ang trabaho namin sa infrastructure na ikino-connect sa mga model ang real-time media.
Nagbi-build din kami sa mismong WebRTC ecosystem, kasama na ang mga established na open-source implementation at ang standard work na nakakatulong para manatiling interoperable ang mga browser, mobile app, at server. Dahil sa napakahalagang nagawa nina Justin Uberti (isa sa mga original architect ng WebRTC) at Sean DuBois (creator at maintainer ng Pion) naging posible para sa mga team na gaya ng team namin na makapag-build sa battle-tested media infrastructure imbes na mag-reinvent ng low-level transport, encryption, at congestion-control behavior. Mapalad kami dahil ngayon, kasama na namin sina Justin at Sean dito sa OpenAI, at tumutulong sila sa pag-guide kung paano namin pagsasamahin ang WebRTC at ang real-time AI.
Sa AI, ang tuloy-tuloy na stream ng audio ang pinakamahalagang feature. Puwede nang magsimulang mag-transcribe, mag-reasoning, tumawag ng mga tool, o mag-generate ng speech ang isang spoken agent habang nagsasalita pa ang user, imbes na maghintay pa ng full upload. Iyan ang kaibahan ng system na conversational ang dating at ng system na parang push-to-talk.
Nang mapili na namin ang WebRTC, ang kasunod na tanong ay kung saan ito ite-terminate (kung saan namin tatanggapin at ima-manage ang WebRTC connection—halimbawa, sa edge) at kung paano iko-connect ang mga session na iyon sa inference backend. Mahalaga ang termination dahil ito ang magse-set kung paano namin iha-handle ang real-time session state, media transport, routing, latency, at failure isolation.
Ang SFU, o selective forwarding unit, ay isang media server na tumatanggap ng isang WebRTC stream mula sa bawat participant at nagfo-forward ng piling mga stream sa iba. Sa model na ito, tini-terminate ng SFU ang isang hiwalay na WebRTC connection para sa bawat participant, at sumasali naman ang AI bilang isa pang participant sa session. Bagay ito para sa mga produktong maraming participant, gaya ng mga group call, classroom, o collaborative meeting. Pinananatili nito sa iisang lugar ang audio codecs, RTCP messages, data channels, recording, at per-stream policy.1
Kahit sa mga client-to-AI product, ang SFU ang madalas na default starting point dahil nagagawa ng mga team na magamit ulit ang isang proven system para sa signaling, media routing, recording, observability, at mga future extension gaya ng human handoff o pagdaragdag ng iba pang participant.
Iba ang workload namin. Karamihan sa mga session ay 1:1—isang user ang nakikipag-usap sa isang model, o isang application ang nakikipag-usap sa isang real-time agent—at may latency sensitivity sa bawat turn. Para sa ganitong traffic, pinili namin ang isang transceiver model: tine-terminate ng WebRTC edge service ang connection ng client at saka kino-convert sa mas simpleng mga internal protocol ang media at mga event para sa model inference, transcription, speech generation, paggamit ng tool, at orchestration.
Sa design na ito, ang transceiver lang ang service na nagma-manage ng WebRTC session state, kasama na rito ang ICE connectivity checks, DTLS handshake, SRTP encryption keys, at session lifecycle. Dito, ang “termination” ay nangangahulugang ang transceiver ang endpoint na kumukumpleto sa mga handshake na iyon at nag-e-encrypt o nagde-decrypt ng media. Dahil napapanatili ang state na iyon sa iisang lugar, nagiging madaling maintindihan ang ownership ng session, at nakakatulong ito para lumawak ang mga backend service na parang mga ordinary service sa halip na magsilbing mga WebRTC peer.
Nang mapili na ang transceiver model, ang naging unang implementation namin ay single Go service na naka-built sa Pion at ito ang nag-handle ng signaling at ng media termination. Pinapagana nito ang ChatGPT voice, ang WebRTC endpoint ng Realtime API, at ilang research project.
Sa operasyon, dalawang trabaho ang ginagawa ng transceiver service :
- Signaling: SDP negotiation, codec selection, mga ICE credential, at session setup
- Media: Tini-terminate ang mga downstream WebRTC connection at mini-maintain ang mga upstream connection sa mga backend service para sa inference at orchestration
Gusto naming gumana ang service na gaya ng iba pa naming infrastructure: sa Kubernetes, kung saan puwedeng madagdagdan o mabawasan ang mga workload, at magpalipat-lipat sa iba’t ibang host habang nagbabago ang demand. Pero hindi gaanong fit sa environment na iyon ang conventional na one-port-per-session na WebRTC model, dahil ang kailangan nito ay malalaking public UDP port range na mahirap i-expose, i-secure, at i-preserve habang nagdaragdag, nag-aalis, o nagre-reschedule ng mga pod.2
Ang unang problema ay ang mismong one-port-per-session model. Kapag mataas ang concurrency, kailangang mag-expose at mag-manage ng napakalalaking UDP port range.
- Ang mga cloud load balancer at Kubernetes services ay hindi dinisenyo para sa libo-libong public UDP port per service. Bawat karagdagang range ng port ay nagdadagdag ng operational complexity sa config ng load balancer, health checking, firewall policy, at safety ng rollout.3
- Mahirap i-sceure ang malalaking UDP port range dahil nae-expand ng mga ito ang surface area na naaabot ng external access at mas nagiging mahirap ma-audit ang network policy.
- Hindi rin ito fit para sa autoscaling. Sa Kubernetes, laging nagdadagdag, nag-aalis, at nagre-reschedule ng mga pod. Kapag kailangang mag-reserve at mag-advertise ng bawat pod ng maraming iba't ibang stable na port, humihina ang flexibility.4
Dahil dito, maraming WebRTC system ang lumilipat sa single UDP port per server, na may application-level demultiplexing sa likod ng port na iyon.5
Nalutas man ng mga single-port-per-server design ang problema sa bilang ng port, nagdulot naman ito ng ikalawang problema: pag-preserve sa ownership ng bawat session sa fleet.
Ang ICE at DTLS ay mga stateful protocol. Ang mga process na lumikha ng session ay kailangang patuloy na makatanggap ng mga packet ng session na iyon para ma-validate nito ang connectivity checks, makumpleto ang DTLS handshake, ma-decrypt ang SRTP, at maproseso ang mga susunod na pagbabago sa session gaya ng mga ICE restart. Kapag sa ibang process napunta ang mga packet para sa same na session, puwedeng hindi gumana ang set up o masira ang media.
Dahil dito, nagkaroon kami ng specific na target: mag-expose ng maliit at fixed na UDP surface sa public internet, habang nira-route pa ang bawat packet sa transceiver na nagmamay-ari sa corresponding na WebRTC session.
May in-evaluate kaming ilang paraan para magawa ito, kasama na rito ang TURN (Traversal Using Relays around NAT), kung saan tine-terminate ng edge relay ang mga client allocation at ifino-forward ang traffic para sa kanila.2
Approach | Pros | Cons |
Unique IP:port per session (kilala rin bilang native direct UDP) | Direct client-to-server media path Walang forwarding layer sa path ng data | Nangangailangan ng isang public UDP port per session Mahirap i-expose at i-secure ang malalaking port range Hindi fit para sa Kubernetes at mga cloud load balancer |
Unique IP:port per server | Mas maliit na public UDP footprint kaysa sa per-session na exposure Makakapag-demultiplex ng maraming session ang isang shared socket per server | Gumagana nang maayos sa single host, pero kung ito lang hindi sapat para sa shared na load-balanced fleet Nakakatulong lang ang session demultiplexing sa single host kapag nakarating na sa host na iyon ang packet; sa load-balanced fleet, puwede pa ring maligaw ang unang packet, kaya kailangan mo pa rin ng predictable na paraan para ma-steer ang bawat session sa process na nagmamay-ari dito |
TURN relay (protocol-terminating) | Kailangan lang marating ng mga client ang address at port ng TURN relay Puwedeng i-centralize sa edge ang policy | Nagdaragdag ng setup round trip ang mga allocation ng TURN Mahirap pa rin i-move o i-recover ang mga allocation sa mga TURN server |
Stateless forwarder + stateful terminator (relay + transceiver ng OpenAI) | Maliit na public UDP footprint Transceiver pa rin ang nagmamay-ari ng buong WebRTC session | Nagdaragdag ng isang forwarding hop bago makarating ang media sa may-aring transceiver Kailangan ng custom coordination sa pagitan ng relay at transceiver |
Inihihiwalay ng ni-release naming architecture ang packet routing mula sa protocol termination. Nagpupunta pa rin sa transceiver ang signaling para i-set up ang session, habang dumdadaan muna sa relay ang media. Ang relay ay isang lightweight na UDP forwarding layer na may maliit na public footprint, at ang transceiver ang stateful WebRTC endpoint sa likod nito.
Hindi nagde-decrypt ng media ang relay, hindi nagra-run ng ICE state machine, at hindi nagpa-participate sa codec negotiation. Nagbabasa ito ng sapat na metadata ng packet para makapili ng destinasyon, saka nito ifino-forward ang packet sa transceiver na may-ari ng session. Normal na WebRTC flow ang nararanasan ng transceiver at ito pa rin ang may full control sa lahat ng protocol state. Mula sa perspective ng client, walang pagbabago sa WebRTC session.
Ang first-packet routing ang mahalagang step sa setup na ito. Kailangang mai-route ng relay ang unang packet mula sa client bago pa magkaroon ng kahit anong session sa mismong path ng packet imbes na mag-pause para sa isang external na lookup service.
Bawat WebRTC session ay mayroon nang protocol-native o built-in na routing hook: ang ICE username fragment, o ufrag, isang maikling identifier na ini-exchange habang sini-set up ang session at inuulit sa mga STUN connectivity check. Gine-generate namin ang server-side na ufrag para maglaman ito ng sapat lang na routing metadata para malaman ng relay ang destination cluster at ang may-aring transceiver.
Sa panahon ng signaling, nag-a-allocate ang transceiver ng session state at nagbibigay ng shared relay VIP at UDP port sa SDP answer. Ang VIP ay isang virtual IP address na nagre-represent sa relay fleet; kapag pinagsama ito at ang port, binibigyan nito ang client ng isang stable na destinasyon, gaya ng `203.0.113.10:3478`, kahit maraming relay instance ang nasa likod nito. Karaniwan na, ang first media-path packet ng client ay STUN (Session Traversal Utilities for NAT) na binding request, na ginagamit ng ICE para ma-verify na nakakarating ang mga packet sa naka-advertise na address.
Sapat na portion lang ng unang STUN packet ang pini-parse ng relay para mabasa ang server ufrag, ma-decode ang routing hint, at mai-forward ang packet sa may-aring transceiver. Bawat transceiver ay nakikinig sa shared UDP socket, ibig sabihin, isang endpoint ng operating system ang naka-bind sa internal IP:port, hindi isang socket kada session. Matapos makagawa ang relay ng isang session mula sa source IP ng client: ang port patungo sa destinasyon ng transceiver na iyon, ang mga DTLS, RTP, at RTCP packet ay nagfo-flow sa loob ng session nang hindi dine-decode ulit ang ufrag.
Sadyang minimal ang session ng relay, na binubuo lang ng in-memory session para ma-inform ang packet forwarding, at ng mga kailangang counter para sa monitoring at mga timer para sa expiration at cleanup ng session. Sa napiling design na ito, napapanatili ang packet routing nang direkta sa path ng packet. Kapag nag-restart ang relay at nawala ang session, nire-rebuild ng susunod na STUN packet ang session mula sa routing hint ng ufrag. Para maging mas reliable pa ito, gumagamit ng Redis cache para ma-hold ang mapping ng <client IP + Port, transceiver IP + Port> kapag na-establish na ang route para ma-recover ito nang mas maaga, bago dumating ang susunod na STUN packet.
Kapag na-reduce na namin ang public UDP surface sa iilang stable na address at port, made-deploy na namin globally ang parehong relay pattern . Ang Global Relay ay ang fleet namin ng mga geographically distributed na relay ingress point na nag-i-implement ng parehong packet-forwarding behavior.
Pinaiikli ng malawak na geographic ingress ang unang hop mula sa client papuntang OpenAI dahil puwedeng pumasok ang packet sa network namin sa isang relay na malapit sa user, kapuwa sa geography at network topology, sa halip na tumawid muna sa public internet patungo sa malayong region. Sa simpleng pananalita, nangangahulugan iyon ng mas mababang latency, mas kaunting jitter, at mas kaunting avoidable loss burst bago makarating sa backbone namin ang traffic.6
Gumagamit kami ng geo at proximity steering ng Cloudflare para sa signaling para makarating sa malapit na transceiver cluster ang initial HTTP o WebSocket request. Ang context ng request ang magdidikta ng location ng session at kung aling Global Relay ingress point ang ia-advertise sa client. Ang SDP answer ang nagbibigay sa Global Relay ng address, habang nasa ufrag naman ang sapat na impormasyon para mai-route ng Global Relay ang media sa tamang cluster at mai-route ito ng relay sa destinasyong transceiver.
Kapuwa inilalagay ng geo-steered signaling at Global Relay ang parehong setup at media sa malapit na entry path habang pinananatiling naka-anchor sa isang transceiver ang session. Nababawasan nito ang round-trip time para sa signaling at para sa unang ICE connectivity check, na direktang nagpapaiikli sa tagal ng paghihintay ng user bago makapagsimula ang pagsasalita.
Dinivelop namin sa Go ang relay service at sinadyang panatilihing minimal ang scope ng implementation. Sa Linux, tinatanggap ng networking stack ng kernel ang mga UDP packet mula sa network interface ng machine at idini-deliver ang mga ito sa socket, ang operating system endpoint na binabasa ng process kapag na-bind na sa IP:Port. Gumagana ang relay sa userspace, kaya binabasa ng regular na Go process ang mga header ng packet mula sa socket na iyon, ina-update ang maliit na amount ng flow state, at ifino-forward ang mga packet nang hindi tine-terminate ang WebRTC. Hindi kami nangailangan ng kahit anong kernel-bypass framework, kung saan direktang makakapag-process ang userspace ng mga poll network queue para sa mas matataas na packet rate pero magdaragdag din ito ng operational complexity.
Mga key design choice:
- Walang protocol termination: Mga STUN header/ufrag lang ang pini-parse ng relay; gumagamit ito ng cached state para sa mga DTLS, RTP, at RTCP, kaya nananatiling opaque ang mga packet.
- Ephemeral state: Nagpapanatili ito ng maliit at may maikling-timeout na in-memory map ng address ng client papunta sa destinasyon ng transceiver para sa flow state at observability.
- Horizontal scalability: Maraming relay instance ang sabay-sabay na gumagana sa likod ng isang load balancer. Hindi hard WebRTC state ang state, kaya ang mga restart ay nagdudulot lang ng minimal na mga traffic drop at mabilis na recovery ng flow.
Mga efficiency measure:
- Ang
SO_REUSEPORTay isang Linux socket option na nagpapahintulot sa maraming relay worker, na nasa iisang makina, na mag-bind sa parehong UDP port. Pagkatapos, idi-distribute ng kernel ang mga papasok na packet sa mga worker na iyon, kaya naiiwasan ang bottleneck dahil sa single na read loop. - Pini-pin ng
runtime.LockOSThreadang bawat UDP-reading goroutine sa isang specific na OS thread. Kapag pinagsama ito at angSO_REUSEPORT, pinapanatili ng mga iyon ang mga packet mula sa parehong flow (ang source at destinasyong IP:Port plus protocol) sa parehong CPU core, kaya nai-improve ang cache locality at nababawasan ang context switching. - Nakakatulong ang mga pre-allocated buffer at minimal na copying na maging mababa ang parsing at allocation overhead para maiwasan ang garbage collection sa Go.
Naha-handle ng implementation na ito ang global real-time media traffic namin nang may maliit na relay footprint, kaya pinanatili namin ang mas simpleng design imbes na gumamit ng kernel bypass route.
Sa architecture na ito, nakakapag-run kami ng WebRTC media sa Kubernetes nang hindi nag-e-expose ng libo-libong UDP port. Mahalaga iyon dahil mas madaling i-secure at i-load balance ang mas maliit at fixed na UDP surface, at posibleng mapalawak ang infastracture nang hindi nagre-reserve ng malalaking public port range. Dahil sa mas maayos na infra support mula sa Kubernetes at mas mahusay na security dahil sa mas maliit na surface area, nape-preserve din ng design na ito ang standard na WebRTC behavior para sa mga client at pinatutunayan na ang mas simpleng SFU design ang tamang default para sa workload namin. Karamihan sa mga session namin ay point-to-point, latency-sensitive, at mas madaling i-scale kapag hindi kailangang maging mga WebRTC peer ang mga inference service.
Ito ang mas malawak na lesson, pinakamagandang magdagdag ng complexity sa minimal na routing layer, hindi sa bawat backend service, at hindi sa custom client behavior. Ang pag-encode ng metadata sa built-in na protocol field ay nagbigay sa amin ng predictable na first-packet routing, maliit na public UDP footprint, at sapat na flexibility para mailagay ang ingress malapit sa mga user sa buong mundo.
May ilang desisyon na lalong naging mahalaga:
- Panatilihin sa edge ang protocol semantics. Standard WebRTC pa rin ang ginagamit ng mga client, kaya nanantiling intact ang interoperability ng browser at mobile.
- Panatilihing nasa iisang lugar ang mga hard session state. Ang transceiver ang may-ari ng ICE, DTLS, SRTP, at lifecycle ng session; nagfo-forward lang ng mga packet ang relay.
- Mag-route gamit ang impormasyong nasa setup na. Ang ICE ufrag ang nagbigay sa amin ng first-packet routing hook nang hindi nagdaragdag ng dependency sa hot-path lookup.
- Mag-optimize para sa common case bago gumamit ng kernel bypass. Ang minimal na Go implementation na may maingat na paggamit ng
SO_REUSEPORT, thread pinning, at low-allocation parsing ay sapat na para sa workload namin.
Ang real-time voice AI ay nakadepende sa infrastructure na hindi halata ang latency. Para sa amin, nangangahulugan ito na kailangan naming baguhin ang WebRTC deployment namin nang hindi binabago ang inaasahan ng mga client sa WebRTC mismo.
May-akda
Mga Reference
2. GitHub - l7mp/stunner: A Kubernetes media gateway for WebRTC(magbubukas sa bagong window)
3. WebRTC Ports in a nutshell [Examples] - BlogGeek.me(magbubukas sa bagong window)
4. Deploy to Kubernetes - LiveKit docs(magbubukas sa bagong window)
6. Cloudflare Calls: millions of cascading trees all the way down(magbubukas sa bagong window)


