<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bordane's Blog]]></title><description><![CDATA[Bordane's Blog]]></description><link>https://bordane.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 14 Sep 2026 14:12:03 GMT</lastBuildDate><atom:link href="https://bordane.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What Is an AI Voice Agent? How Voice Agents Actually Work]]></title><description><![CDATA[An AI voice agent is software you can talk to. It listens to you speak, reasons about what you said, and talks back in real time — holding a natural spoken conversation instead of marching you through]]></description><link>https://bordane.hashnode.dev/what-is-an-ai-voice-agent-how-voice-agents-actually-work</link><guid isPermaLink="true">https://bordane.hashnode.dev/what-is-an-ai-voice-agent-how-voice-agents-actually-work</guid><category><![CDATA[WebRTC]]></category><category><![CDATA[AI]]></category><category><![CDATA[software development]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[James bordane]]></dc:creator><pubDate>Wed, 29 Jul 2026 15:41:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63af01a3359f463d43abebd0/1f731fa1-bf28-4091-9cfb-9ae2b69deb81.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An AI voice agent is software you can talk to. It listens to you speak, reasons about what you said, and talks back in real time — holding a natural spoken conversation instead of marching you through a phone menu.</p>
<p>That definition is the easy part. Every explainer on the internet will tell you a voice agent is speech-to-text, plus a language model, plus text-to-speech.</p>
<p>Here is the part almost nobody writes down: <strong>a voice agent is a latency and connectivity problem, not a prompt problem.</strong></p>
<p>The language model is the easy 80%. Wiring three models together is a weekend project. The hard part — the part that decides whether your agent feels human or just goes silent in a customer's office — is the plumbing underneath: how the audio actually travels, how fast, and whether it can connect at all.</p>
<p>This guide covers the whole stack. You'll get the standard architecture, the honest 2026 trade-offs, a real latency budget with numbers we measured ourselves, and the transport layer that the top-ranking guides leave out entirely.</p>
<blockquote>
<p><strong>TL;DR:</strong> An AI voice agent hears you (speech-to-text), thinks (an LLM), and speaks back (text-to-speech), coordinated by an orchestrator that manages turns and interruptions — with every stage streamed to stay under the ~800 ms that keeps a conversation feeling natural. The layer most guides skip is transport: the audio rides WebRTC, and on real-world networks it needs a TURN relay, or the agent connects and goes silent.</p>
</blockquote>
<h2>What Is an AI Voice Agent?</h2>
<p>An AI voice agent is an autonomous, voice-first system that holds a natural spoken conversation, reasons about it in real time with a large language model, and takes action across connected systems — without a human scripting each turn. It combines speech-to-text, an LLM, and text-to-speech, connected over a real-time media transport like WebRTC.</p>
<p>That last clause is the one you rarely see, and it is the whole reason this article exists.</p>
<p>The difference between a voice agent and the older systems it replaces is intent. An IVR forces callers down a fixed menu tree — "press 1 for sales." A chatbot handles typed text. A voice agent understands natural spoken language, decides what to do, calls your backend, and answers in speech, per the distinction drawn across <a href="https://aircall.io/blog/what-is-an-ai-voice-agent/">Aircall's 2026 explainer</a> and <a href="https://deepgram.com/learn/what-exactly-is-an-ai-voice-agent">Deepgram's 2026 guide</a>.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/19r6vnu6y9f13iio1r2n.png" alt="Image description" style="display:block;margin:0 auto" />

<p>So a voice agent is a loop, not a black box. It hears, it thinks, it speaks, and something has to conduct all three in the right order, fast enough that you don't notice the seams.</p>
<p>That conductor is the orchestrator, and it does more than pass data between stages. Let's break the loop down stage by stage.</p>
<h2>How Do Voice Agents Work? The Four Stages</h2>
<p>A voice agent runs four real-time stages in a tight loop: speech-to-text transcribes what you say, an LLM decides the reply and calls any tools, text-to-speech speaks it, and an orchestrator manages when a turn ends and when to stop for an interruption. Streaming overlaps the stages so the reply starts before the model has finished thinking.</p>
<p>Here's each stage and what it's actually responsible for.</p>
<p><strong>Speech-to-text (STT)</strong> turns your audio into words the model can read. In a good agent it runs continuously, emitting partial transcripts as you talk rather than waiting for you to finish.</p>
<p><strong>The LLM</strong> reads the transcript, decides what to say, and — when needed — calls tools: look up an order, book the appointment, check inventory. This is the "brain," but as you'll see in the latency section, it is rarely the slow part.</p>
<p><strong>Text-to-speech (TTS)</strong> turns the reply back into audio. Streaming TTS starts speaking the first words while the rest of the sentence is still being generated, which is what keeps the pause short.</p>
<p><strong>The orchestrator</strong> is the unsung hero. It decides when your turn has ended (turn detection), starts the reply, and — critically — stops the agent mid-sentence the instant you interrupt. That interruption behavior is called barge-in, and getting it right is most of what makes an agent feel human rather than robotic.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/putyl0guymvui1j97k30.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Notice the theme: everything streams. A voice agent that waits for each stage to fully finish before starting the next one feels broken, because the silence stacks up.</p>
<p><a href="https://www.assemblyai.com/blog/voice-agent-architecture">AssemblyAI's April 2026 architecture breakdown</a> puts a number on it: a naive, non-streaming pipeline adds two to four seconds of dead air per turn. Nobody waits four seconds for a "hello."</p>
<p>That's the standard model. But in 2026 there's a real architectural fork in the road — one model or three?</p>
<h2>Cascaded Pipeline vs Speech-to-Speech</h2>
<p>There are two ways to build the "hear-think-speak" loop, and the choice is a genuine engineering trade-off in 2026. A <strong>cascaded pipeline</strong> chains three separate models — STT, then LLM, then TTS — with readable text between each step. A <strong>speech-to-speech (S2S)</strong> model does it in one shot: audio in, audio out, no text in the middle.</p>
<p>Cascade gives you a text artifact at every step, so you can log it, moderate it, filter it, and route on it. Speech-to-speech often feels more natural and can be faster, but it's harder to debug, more expensive, and less transparent.</p>
<p>As of April 2026, cascade still dominates production, per <a href="https://deepgram.com/learn/speech-to-speech-vs-cascade-voice-agent-architecture">Deepgram</a> and <a href="https://softcery.com/lab/ai-voice-agents-real-time-vs-turn-based-tts-stt-architecture">Softcery's lab tests</a>. Here is how the two stack up on the numbers people actually argue about.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Cascaded (STT → LLM → TTS)</th>
<th>Speech-to-speech (one model)</th>
</tr>
</thead>
<tbody><tr>
<td>Time to first audio</td>
<td>~1.5–3 s in production (Softcery/Deepgram, 2026)</td>
<td>0.78–2.98 s across models (Softcery, Apr 2026)</td>
</tr>
<tr>
<td>Cost per minute</td>
<td>~$0.05–0.15 (Softcery, Apr 2026)</td>
<td>~$0.15–0.60 for premium realtime (Softcery, Apr 2026)</td>
</tr>
<tr>
<td>Transparency</td>
<td>High — text at every stage to log and filter</td>
<td>Low — audio in, audio out, harder to audit</td>
</tr>
<tr>
<td>Debuggability</td>
<td>Easy — inspect the transcript and the reply</td>
<td>Hard — no intermediate text artifact</td>
</tr>
<tr>
<td>Best for</td>
<td>Transactional, regulated, tool-heavy agents</td>
<td>Natural-feeling, latency-sensitive chat</td>
</tr>
</tbody></table>
<p>The cost gap is not a rounding error. Softcery's April 2026 measurements show a spread of up to <strong>182×</strong> between the cheapest cascaded stack and premium realtime speech-to-speech models — the difference between a fraction of a cent and roughly thirty cents a minute.</p>
<p>Cascade runs about $0.05–$0.15 per minute and produces a clean text log at every step; premium speech-to-speech models run roughly $0.15–$0.60 per minute and feel more natural but are far harder to audit. Their time-to-first-audio ranges from about 0.78 seconds on the fastest model to nearly 3 seconds on others, versus roughly 1.5–3 seconds end-to-end for a well-built cascade.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/u13ukbe5qy5noscx10jk.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Here's what most teams actually do: a hybrid. Use a speech-to-speech model for the natural-feeling opener, then fall back to a cascaded pipeline for the transactional turns where you need a text log and tool calls.</p>
<p>Now, every source above measures the models. None of them measures the wire the audio travels on. That's the layer we're built to explain.</p>
<h2>The Layer Most Guides Skip: How the Audio Actually Travels</h2>
<p>Here is the gap. The best voice-agent architecture guides — <a href="https://www.assemblyai.com/blog/voice-agent-architecture">AssemblyAI's</a> (April 2026) and <a href="https://deepgram.com/learn/what-exactly-is-an-ai-voice-agent">Deepgram's</a> (2026) among them — walk through STT, LLM, and TTS in detail and then stop. Deepgram names WebRTC as a transport and moves on. None of them explains the layer underneath: how the audio actually gets from a user's microphone to your agent and back.</p>
<p>So let's fill it in.</p>
<p>Audio has to ride something. There are three real choices for moving real-time voice, and they are not interchangeable.</p>
<p><strong>WebRTC</strong> is the transport built for exactly this job. It runs over UDP, was designed for low-latency real-time media, handles packet loss gracefully, and includes the machinery to punch through firewalls. The emerging consensus for production voice agents is WebRTC over WebSockets.</p>
<p><strong>WebSockets</strong> run over TCP. They're perfect for control messages, transcripts, and prototypes — but TCP's head-of-line blocking means one late packet stalls everything behind it, which is exactly the wrong property for live audio. Fine for a demo; not what production settles on.</p>
<p><strong>SIP</strong> is the telephony path. If your agent answers actual phone calls over the PSTN, SIP is in the picture — it's a live transport decision for phone-based agents, as <a href="https://relinns.com/blogs/webrtc-vs-sip-for-ai-voice-agents">relinns' 2026 comparison</a> lays out. For a web or app-based agent, WebRTC is the default.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/osiiqoblvk29n64hkxm8.png" alt="Image description" style="display:block;margin:0 auto" />

<p>So for the agents most people are building — web and app — the answer is WebRTC. But choosing WebRTC is where the connectivity problem begins, not where it ends.</p>
<p>Because WebRTC prefers a direct UDP path, and on a lot of real-world networks, that direct path simply doesn't exist. Hold that thought — we'll get to why in two sections. First, let's put real numbers on "fast enough."</p>
<h2>The Real Latency Budget (With Our Measured Numbers)</h2>
<p>Here's the target: keep the end-to-end response under about 800 milliseconds at the 95th percentile, from the moment you stop talking to the moment the agent starts. Under 500–700 ms feels natural; past that, it starts to feel like a bad phone connection.</p>
<p>Those thresholds come from practitioners. AssemblyAI's April 2026 breakdown budgets <strong>600–900 ms</strong> for a fully streamed pipeline; <a href="https://prodinit.com/blog/production-voice-ai-agents-latency-architecture">Prodinit's 2026 production guide</a> puts the reliability floor at <strong>sub-800 ms p95</strong> and calls sub-250 ms p50 achievable. AssemblyAI also notes that responses beyond 500–700 ms "start to feel unnatural."</p>
<p>Here's a dated budget, stage by stage, from the published sources next to <strong>our own measured build</strong>.</p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Industry budget (AssemblyAI, 2026-04-29)</th>
<th>Our measured run (2026-07-14, M1 Pro, cloud)</th>
</tr>
</thead>
<tbody><tr>
<td>Speech-to-text</td>
<td>200–500 ms</td>
<td><strong>~1,200–1,800 ms</strong> (the bottleneck)</td>
</tr>
<tr>
<td>LLM (first token)</td>
<td>150–400 ms</td>
<td>~800–1,400 ms</td>
</tr>
<tr>
<td>Text-to-speech (first audio)</td>
<td>200–400 ms</td>
<td>~500–700 ms</td>
</tr>
<tr>
<td>Network / transport</td>
<td>50–150 ms</td>
<td>included above</td>
</tr>
<tr>
<td><strong>End-to-end</strong></td>
<td><strong>600–900 ms</strong></td>
<td><strong>2,500–3,900 ms to first audio</strong></td>
</tr>
<tr>
<td>Barge-in (interruption)</td>
<td>—</td>
<td><strong>≈ 1 ms</strong> (server-side VAD)</td>
</tr>
</tbody></table>
<p>Let me be honest about our numbers. <strong>We built a real voice agent and measured it</strong> on 2026-07-14 — a MacBook Pro, 16 GB RAM, on a residential network, running a cloud stack of OpenAI GPT plus Whisper STT plus OpenAI TTS with streaming enabled. These are our numbers on our hardware, not a vendor performance guarantee. The full build and method are in our <a href="https://dev.to/alakkadshaw/build-an-ai-voice-agent-in-typescript-cloud-or-100-local-one-config-swap-40f1">TypeScript voice-agent tutorial</a>.</p>
<p>End to end, we measured roughly <strong>2.5–3.9 seconds</strong> to first audio on the default buffered pipeline. That's slower than the streamed-ideal industry budget — roughly 200–500 ms for STT, 150–400 ms for the LLM's first token, 200–400 ms for TTS, and 50–150 ms of network, about 600–900 ms end to end (AssemblyAI, 2026-04-29). The gap between that textbook 600–900 ms and a real 2.5–3.9 s is buffering and cold starts, not model quality — which is exactly the point.</p>
<p>And here's the insight that reorganizes how you optimize: <strong>STT dominates.</strong> Buffered Whisper alone ate 1.2–1.8 seconds — the single biggest slice. The LLM (<del>0.8–1.4 s) and TTS (</del>0.5–0.7 s) were not our bottleneck; transcription was. If you're trying to make an agent feel faster, the streaming STT model is usually the highest-leverage lever, not a bigger LLM.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/iahjpw3k3rsdt1n4bih9.png" alt="Image description" style="display:block;margin:0 auto" />

<p>One more number worth staring at: barge-in. When we talked over the agent, the server's voice-activity detection cancelled its speech within about <strong>1 millisecond</strong> of detecting our voice. Interruption handling isn't a latency problem — it's an architecture problem, and it belongs on the server, close to the media.</p>
<p>Which brings us back to the connectivity thread. All of this latency assumes the audio connects in the first place. On a lot of networks, it doesn't.</p>
<h2>Why Voice Agents Fail on Corporate, Hospital, and Mobile Networks</h2>
<p>This is the failure mode that ships to production and blindsides teams: the agent works perfectly on your Wi-Fi and goes silent in a customer's office. The call connects, the transcript even flows — and there's no audio.</p>
<p>It's not a bug in your code. It's the network, and it's predictable.</p>
<p>WebRTC wants a direct peer-to-peer path over UDP. Restrictive networks break that in two ways. Many corporate, hospital, and mobile carrier networks use <strong>symmetric NAT</strong>, which scrambles the address mapping so the two sides can't agree on where to send packets. Others simply <strong>block the UDP ports</strong> WebRTC reaches for.</p>
<p>Here's the pattern we see over and over: it works on the developer's home Wi-Fi and dies the moment a real user is on a corporate LAN or on cellular. Same code, different network, silent call.</p>
<p>Now add the twist that makes this worse for agents specifically. A browser-to-browser call can sometimes fall back to a direct path between two consumer networks. <strong>A server-side voice agent has no peer-to-peer fallback</strong> — one end is a machine in a data center — so when the direct path fails, there is no plan B on the same wire. The audio has to be relayed, which is why agent media is almost always relayed in production. We wrote up that reasoning in depth in <a href="https://dev.to/alakkadshaw/turn-for-ai-voice-agents-why-agent-traffic-is-almost-100-relay-3meo">TURN for AI voice agents</a>.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/lwj2sz08myh1ob634jzu.png" alt="Image description" style="display:block;margin:0 auto" />

<p>So the uncomfortable truth is that "it works on my machine" is the default state of a WebRTC voice agent, and it lies to you. The networks where it fails are precisely the networks your paying customers sit on.</p>
<p>The fix is a relay. And that's where STUN and TURN come in.</p>
<h2>STUN, TURN, and What a Production Agent Actually Needs</h2>
<p>Two acronyms do the connectivity work, and they are not the same thing. <strong>STUN</strong> helps a client discover its own public address so two peers can try a direct connection. <strong>TURN</strong> is the fallback that actually <strong>relays</strong> the media through a server when the direct path fails. STUN discovers; TURN relays. Confusing the two is the most common mistake in this whole topic.</p>
<p>For a production voice agent, you need TURN. The direct path fails often enough on real networks — and a server-side agent has no peer-to-peer fallback — that a relay isn't a nice-to-have, it's the thing standing between "works in the demo" and "works for customers."</p>
<p>There's a detail that matters for the hardest networks: <strong>TURN over TLS on port 443</strong>. Locked-down corporate and hospital firewalls that block everything else usually still allow outbound 443, because that's where normal HTTPS lives. A TURN server that speaks TURNS on 443 looks like ordinary web traffic and gets through where raw UDP is dead on arrival.</p>
<p>So how do you get a TURN server? You have two honest paths, and both are legitimate.</p>
<p><strong>Run your own</strong> with an open-source server like <a href="https://www.metered.ca/blog/coturn/">coturn</a>. It's free software, but you own the config, the TLS certificates, the ports, the capacity planning, and the bandwidth bill. It's real DevOps work, and the bandwidth adds up.</p>
<p><strong>Use a managed TURN service</strong> and skip the operations. You can start free — <a href="https://www.metered.ca/tools/openrelay/">Open Relay</a> gives you <strong>20 GB/month of TURN bandwidth</strong> at no cost, on ports 80 and 443 with TURNS, which is plenty for small workloads. When you outgrow it, <a href="https://www.metered.ca/stun-turn">Metered's TURN service</a> starts with a 500 MB free trial and then runs Growth at $99 for 150 GB, Business at $199 for 500 GB, and Enterprise at $499 for 2 TB, across 31+ regions and 100+ edge locations.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kfuwf1z3kjflei5zz5wt.png" alt="Image description" style="display:block;margin:0 auto" />

<p>You don't have to decide today. The point is that a TURN relay is part of the voice-agent stack, full stop — not an optional extra you bolt on after launch, but the layer that makes the other four work on real networks. If you want to check whether your own agent's path holds up, you can <a href="https://www.metered.ca/turn-server-testing">test a TURN connection</a> before your users do it for you.</p>
<p>That's the whole stack, actually. Let's map it.</p>
<h2>The Voice-Agent Stack You Actually Need</h2>
<p>A voice-agent stack has six layers, and most guides only cover the first four. You need speech-to-text, an LLM, text-to-speech, an orchestrator for turn-taking and barge-in, a media-transport layer (WebRTC) to move the audio, and — for production reliability — a TURN relay plus signalling to establish the connection.</p>
<p>The first four are the conversation. The last two are the connection. Skip the connection layers and you get an agent that demos beautifully and fails in the field.</p>
<p>Metered is <strong>not</strong> a voice-agent platform, and this isn't a pitch to replace your STT, LLM, or TTS. It is the infrastructure under the stack — the transport and connectivity layer that sits beneath <em>any</em> voice agent, whether you built it yourself or bought a hosted one.</p>
<p>That layer is two things. <strong>TURN</strong> relays the media so the agent connects on real networks; Open Relay covers the free tier and the <a href="https://www.metered.ca/stun-turn">Metered TURN service</a> covers scale, regions, and analytics. And <strong>signalling</strong> is the coordination channel that helps the two sides find each other and exchange connection details before the media flows.</p>
<p>On signalling, one honest note so you can plan the whole connection layer: <a href="https://www.metered.ca/tools/openrelay/webrtc-signaling-server">Metered Realtime</a> is a managed signalling service you can <strong>start free</strong>, with an MIT-licensed open-source client.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/13su7ma6s9xtk4pta214.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Read the stack top to bottom and the thesis of this whole guide falls out. The top four layers are where the intelligence lives, and they're mostly solved by picking good models. The bottom two are where agents actually break, and they're an infrastructure problem — latency and connectivity — not a prompt problem.</p>
<p>So how do you assemble all six? You have three routes.</p>
<h2>How to Build a Voice Agent</h2>
<p>build it open-source.** Assemble it yourself for full control and provider freedom. Our own free, open-source TypeScript SDK, <a href="https://llmrtc.org">LLMRTC</a>, handles the voice-agent hard parts — WebRTC transport, server-side voice-activity detection, natural barge-in, tool calling, and a provider-agnostic pipeline so you can swap OpenAI, Anthropic, Gemini, or local models by config. It's Apache-2.0 and truly free, built by a team that runs production WebRTC infrastructure. Its own docs tell you to put a TURN server in front of it for users behind NAT — because, as this article has hammered, you need one.</p>
<p>We proved that route end-to-end: our <a href="https://dev.to/alakkadshaw/build-an-ai-voice-agent-in-typescript-cloud-or-100-local-one-config-swap-40f1">TypeScript voice-agent tutorial</a> builds a real agent and swaps the entire stack from cloud to 100% local with one config change. And if you're wiring a browser straight to a hosted realtime model, our walkthrough of <a href="https://medium.com/@jamesbordane57/openai-realtime-api-over-webrtc-how-it-works-when-you-need-turn-2a3abcdda451">OpenAI Realtime over WebRTC</a> shows exactly when TURN enters the picture — the direct browser-to-OpenAI path connected with no <code>iceServers</code> at all, and TURN came back the moment we owned a leg of the connection.</p>
<p><strong>Route 2: hybrid.</strong> Use a platform or a hosted model for the conversation, and own the connectivity layer yourself — WebRTC transport, TURN relay, signalling — so you control quality, regions, and cost on the part that actually breaks. This is where most serious deployments land, and it's the route where an independent TURN service earns its keep.</p>
<p>Whichever route you take, the connection layer is yours to get right. No platform makes the network problem disappear if you own any leg of the WebRTC path.</p>
<h2>Frequently Asked Questions</h2>
<h3>What is an AI voice agent?</h3>
<p>An AI voice agent is an autonomous, voice-first system that holds a natural spoken conversation, reasons about it in real time with a large language model, and takes action across connected systems — without a human scripting each turn. It combines speech-to-text, an LLM, and text-to-speech, connected over a real-time media transport like WebRTC.</p>
<h3>How do AI voice agents work?</h3>
<p>They run four real-time stages: speech-to-text transcribes the user, an LLM decides the response and calls tools, text-to-speech speaks it, and an orchestrator manages turn-taking and interruptions. Streaming overlaps the stages to cut latency. A media-transport layer — usually WebRTC — carries the audio between the user and the agent.</p>
<h3>How much latency is acceptable for a voice agent?</h3>
<p>Keep end-to-end response under about 800 ms at p95, from end of speech to first audio; under 500–700 ms feels natural, and sub-250 ms p50 is achievable with a fully streamed stack. A naive, non-streaming pipeline adds 2–4 seconds of dead air, which breaks the conversation. Transport is part of that budget.</p>
<h3>Do voice agents need a TURN server?</h3>
<p>In production, usually yes. A server-side agent's WebRTC media has no peer-to-peer fallback, and many corporate and mobile networks block direct UDP, so the audio must be relayed through a TURN server. You can start free with Open Relay's 20 GB/month and move to a managed TURN service for regions, capacity, and analytics as you scale.</p>
<h3>Why does my voice agent work locally but fail on office or hospital Wi-Fi?</h3>
<p>Restrictive networks use symmetric NAT and block the UDP ports WebRTC needs, so the direct media path can't form. A server-side agent has no peer-to-peer fallback, so its audio is almost always relayed — which means it needs a TURN server. Without one, the call connects and then goes silent.</p>
<h3>What's the difference between speech-to-speech and a cascaded pipeline?</h3>
<p>A cascaded pipeline chains three models — STT, LLM, TTS — giving you a readable text artifact at each step to log, filter, or route. Speech-to-speech uses one model from audio in to audio out: often more natural, but harder to debug, more expensive, and less transparent. As of April 2026, cascade still dominates production.</p>
<h3>What's the difference between a voice agent and an IVR or chatbot?</h3>
<p>An IVR forces callers down fixed menus ("press 1 for sales"); a chatbot handles typed text. A voice agent understands natural spoken language, reasons with an LLM, takes actions in backend systems, and replies in natural speech — no scripted menu tree, and it works over the phone or the web.</p>
<h2>The Bottom Line</h2>
<p>An AI voice agent is four models in a loop — hear, think, speak, and a conductor to run them — riding on two layers of connectivity most guides never mention. Get the models right and you have a demo. Get the transport and relay right and you have a product.</p>
<p>That's the whole argument: <strong>voice agents are a latency and connectivity problem, not a prompt problem.</strong> The prompt is the part you'll finish first. The media path is the part that decides whether your agent feels human or goes silent on the exact networks your customers use.</p>
<p>So build the conversation however you like — a platform, open-source, or a hybrid. But own the connection layer. Start free on <a href="https://www.metered.ca/tools/openrelay/">Open Relay's 20 GB/month</a>, and when real users behind real firewalls show up, <a href="https://www.metered.ca/stun-turn">Metered's TURN service</a> relays the audio across 31+ regions so your agent connects everywhere — not just on your Wi-Fi.</p>
<hr />
<h2><strong>About the author:</strong> This guide was written by James Bordane</h2>
]]></content:encoded></item><item><title><![CDATA[OpenAI Realtime API over WebRTC: How It Works + When You Need TURN]]></title><description><![CDATA[This story was published on medium: OpenAI Realtime API over WebRTC
Do you need a TURN server to use the OpenAI Realtime API over WebRTC? It is the first question a WebRTC-experienced developer asks, ]]></description><link>https://bordane.hashnode.dev/openai-realtime-api-over-webrtc-how-it-works-when-you-need-turn</link><guid isPermaLink="true">https://bordane.hashnode.dev/openai-realtime-api-over-webrtc-how-it-works-when-you-need-turn</guid><category><![CDATA[AI]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[WebRTC]]></category><category><![CDATA[#ai-tools]]></category><dc:creator><![CDATA[James bordane]]></dc:creator><pubDate>Mon, 20 Jul 2026 19:11:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63af01a3359f463d43abebd0/0f71f80f-9ed8-4664-bd9e-ee0adade7dc1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This story was published on medium:</em> <a href="https://medium.com/@jamesbordane57/openai-realtime-api-over-webrtc-how-it-works-when-you-need-turn-2a3abcdda451"><em>OpenAI Realtime API over WebRTC</em></a></p>
<p>Do you need a TURN server to use the OpenAI Realtime API over WebRTC? It is the first question a WebRTC-experienced developer asks, and the answer is stranger than yes or no: on the direct browser-to-OpenAI path you do not — and you could not add your own TURN even if you wanted to.</p>
<p>But the moment you build the architecture most production voice agents actually ship, TURN comes back onto the critical path.</p>
<p>We map the real connection topologies, walk the WebRTC handshake with a complete working example we executed against the live API, and give you a decision table for exactly when a TURN server is mandatory versus irrelevant.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9v1ebgyy4rbfnzbzadsf.png" alt="Image description" style="display:block;margin:0 auto" />

<blockquote>
<p><strong>TL;DR:</strong> The OpenAI Realtime API runs over WebRTC, WebSocket, or SIP (OpenAI docs, 2026-07-17). On the direct browser-to-OpenAI WebRTC path you do not need your TURN server: OpenAI uses public endpoints, host candidates only, and a TCP/443 fallback, so you can't add TURN anyway. The moment you own a WebRTC leg (browser to your server, or a Python <code>aiortc</code> agent), TURN over TLS on 443 is <strong>mandatory</strong> on restrictive networks.</p>
</blockquote>
<h2>Does the OpenAI Realtime API need a TURN server?</h2>
<p>No — not on the direct path, and yes — the instant you add your own server.</p>
<p>If your browser talks straight to OpenAI, OpenAI owns the WebRTC endpoint. It publishes public, reachable addresses and handles restrictive networks with its own TCP/443 fallback. There is no TURN server for you to configure.</p>
<p>If you put your own server in the middle — to hold your API key, add tools and guardrails, record calls, or swap providers — you now own a browser-to-your-server WebRTC connection. That leg needs your STUN and TURN, exactly like any other WebRTC app.</p>
<p>Most real production voice agents are the second case. So the honest answer is "usually yes — but probably not for the reason you'd expect, and not on the leg you'd expect."</p>
<h2>What the OpenAI Realtime API is in 2026</h2>
<p>The Realtime API is OpenAI's low-latency, speech-to-speech interface for building a <a href="https://developers.openai.com/api/docs/guides/realtime">realtime voice AI</a> agent. It went generally available on <strong>2025-08-28</strong> alongside the first production model, <code>gpt-realtime</code> (OpenAI, accessed 2026-07-17).</p>
<p>As of <strong>2026-07-17</strong>, the catalog lists <code>gpt-realtime-2</code> as the default realtime model, with <code>gpt-realtime-2.1</code> and a cheaper <code>gpt-realtime-2.1-mini</code> as the current point releases, plus specialized <code>gpt-realtime-translate</code> and <code>gpt-realtime-whisper</code> models (developers.openai.com, accessed 2026-07-17). OpenAI ships these fast — four point releases in about eleven months — so pin a specific model and date it rather than trusting "the latest."</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bf4mazb4k4i8pvlf77u1.png" alt="Image description" style="display:block;margin:0 auto" />

<p>The API is reachable over three transports, and OpenAI gives explicit guidance on each. This choice decides which leg of your system owns NAT traversal, so read the table with that lens.</p>
<table>
<thead>
<tr>
<th>Transport</th>
<th>OpenAI's stated use (verbatim)</th>
<th>Endpoint</th>
</tr>
</thead>
<tbody><tr>
<td><strong>WebRTC</strong></td>
<td>"Use for browser and mobile clients that capture or play audio directly."</td>
<td><code>POST /v1/realtime/calls</code></td>
</tr>
<tr>
<td><strong>WebSocket</strong></td>
<td>"Use when your server already receives raw audio from a media pipeline, call system, or worker."</td>
<td><code>wss://api.openai.com/v1/realtime</code></td>
</tr>
<tr>
<td><strong>SIP</strong></td>
<td>"Use for telephony voice agents."</td>
<td>SIP into <code>/v1/realtime</code></td>
</tr>
</tbody></table>
<p><em>Table: OpenAI Realtime API transports and guidance, quoted from the official Realtime guide (developers.openai.com, accessed 2026-07-17).</em></p>
<p>The load-bearing takeaway: WebRTC is the client-edge transport, WebSocket is the server-side transport. Where your audio originates tells you which transport to use — and whether a browser is a WebRTC peer at all.</p>
<h2>How the browser-to-OpenAI WebRTC connection actually works</h2>
<p>The direct WebRTC flow skips the signalling server you would normally build. There is no WebSocket handshake to negotiate the call; OpenAI uses plain HTTP for the SDP exchange (OpenAI WebRTC guide, accessed 2026-07-17).</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bmd5n5latj3nixykswtc.png" alt="Image description" style="display:block;margin:0 auto" />

<p>It runs in four moves, and the code below is the complete flow. We executed it end-to-end against the live API on 2026-07-17; the field names, status codes, and connection states that follow were observed.</p>
<p>First, your backend mints a short-lived client secret so your real API key never touches the browser.</p>
<pre><code class="language-js">// server.js (Node 18+) — your standard API key stays server-side.
const r = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    session: {
      type: "realtime",
      model: "gpt-realtime-2.1-mini",        // pin a model and date it
      audio: { output: { voice: "marin" } },
    },
  }),
});
const token = await r.json(); // → { value: "ek_…", expires_at, session }
// hand token.value to the browser; it expires quickly by design
</code></pre>
<p>Second, the browser creates an <code>RTCPeerConnection</code>, attaches the mic, opens the events channel, and <strong>POSTs its raw SDP offer</strong> to OpenAI — which returns the SDP answer in the HTTP response body with a <code>201 Created</code>.</p>
<pre><code class="language-js">// browser — fetch the ephemeral key from YOUR backend, never OpenAI directly
const { value: EPHEMERAL_KEY } = await (await fetch("/token", { method: "POST" })).json();

const pc = new RTCPeerConnection(); // note: no iceServers passed — this is the whole point

pc.ontrack = (e) =&gt; { audioEl.srcObject = e.streams[0]; };        // model audio out
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTrack(mic.getAudioTracks()[0], mic);                        // your mic in

const events = pc.createDataChannel("oai-events");                // JSON events channel
events.onmessage = (e) =&gt; {
  const ev = JSON.parse(e.data); // session.created, response.*, input_audio_buffer.*
  if (ev.type === "response.output_audio_transcript.done") console.log(ev.transcript);
};

await pc.setLocalDescription(await pc.createOffer());
const resp = await fetch("https://api.openai.com/v1/realtime/calls", {
  method: "POST",
  body: pc.localDescription.sdp,
  headers: { Authorization: `Bearer ${EPHEMERAL_KEY}`, "Content-Type": "application/sdp" },
});
await pc.setRemoteDescription({ type: "answer", sdp: await resp.text() }); // 201 + answer SDP
</code></pre>
<p>Third, session updates, tool calls, and transcripts flow as JSON over the data channel named <code>oai-events</code>, using the same schema as the WebSocket API. Fourth, audio is just a normal media track in each direction (OpenAI WebRTC guide, accessed 2026-07-17).</p>
<p>Here is what our live run observed, in order: the secret minted as <code>{ value, expires_at, session }</code>; the SDP exchange returned <strong>201 Created</strong>; ICE went <code>checking → connected</code> with <strong>no ICE servers configured</strong>; <code>oai-events</code> opened; and the model answered our first <code>response.create</code> out loud. OpenAI's server-side voice activity detection then took further turns off the incoming audio stream — the full round trip, working.</p>
<p>Notice what is missing: you never pass <code>iceServers</code> to that <code>RTCPeerConnection</code>. That omission is deliberate, and it is the key to the entire TURN question.</p>
<h2>The architecture question: where do STUN and TURN come in?</h2>
<p>On the direct browser-to-OpenAI path, STUN and TURN do not come in at all — from your side. OpenAI terminates WebRTC server-side at publicly reachable endpoints and returns <strong>host candidates only, with no STUN or TURN server</strong> (<a href="https://webrtchacks.com/how-openai-does-webrtc-in-the-new-gpt-realtime/">webrtcHacks teardown of the GA <code>gpt-realtime</code> stack</a>, dated 2025-09-23).</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/mzqspse16c4gxt4ku4yi.png" alt="Image description" style="display:block;margin:0 auto" />

<p>The teardown found OpenAI advertising multiple public Azure datacenter endpoints and connecting clients directly to them over <strong>UDP on port 3478 and TCP on port 443</strong> — with 443/TCP added at GA specifically to pass firewalls that block UDP and non-web ports (webrtcHacks, 2025-09-23).</p>
<p>That design has a clean consequence. Because OpenAI's endpoint is public and ships its own TCP/443 fallback, the browser-to-OpenAI hop traverses most NATs and many corporate firewalls <strong>without any TURN server on your side</strong>. Our executed run is the proof in miniature: ICE reached <code>connected</code> with no ICE servers configured at all.</p>
<p>And you could not add one if you wanted to. OpenAI controls the answer SDP, so there is no place to inject your relay. If WebRTC muscle memory has you reaching for an <code>iceServers</code> block here, there is nothing for it to do — on this topology a TURN server is just not necessary</p>
<p>This is why direct-path failures reported in OpenAI's community forums read as transient service issues, not NAT problems — the direct path rarely fails on NAT because OpenAI engineered the firewall escape hatch into its own endpoint.</p>
<h2>When you DO need TURN: the moment you own a WebRTC leg</h2>
<p>Here is the turn. Most production voice agents do <strong>not</strong> send browser audio straight to OpenAI. They insert a server in the middle — and that server changes everything about connectivity.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0hrkak1y8xalxhd5gzxv.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Why add a server? To keep your API key off the client, add tools and guardrails, run server-side voice activity detection and barge-in, record or transcribe, bridge telephony, or swap the model provider without shipping a new client. All sensible reasons — and all of them create a second WebRTC connection that you own.</p>
<p>On the <strong>browser-to-your-server</strong> leg, <em>you</em> are the WebRTC endpoint. That means you own NAT traversal. A public-IP media server handles most users through <a href="https://medium.com/@jamesbordane57/what-is-a-turn-server-045f186f88a3">host and server-reflexive candidates</a> — but users on <strong>symmetric NAT, UDP-blocked corporate, hospital, or bank networks, or restrictive Wi-Fi cannot connect without a TURN relay</strong>, ideally TURN over TLS on port 443 so it looks like ordinary HTTPS.</p>
<p>This is not my claim alone. Python's <a href="https://github.com/aiortc/aiortc">aiortc</a> uses a standard <code>RTCConfiguration</code> with <code>iceServers</code>, and the same NAT rules apply on its browser-facing leg.</p>
<p>So the decision is not "does OpenAI Realtime need TURN." It is "does <em>my</em> architecture put a WebRTC leg under <em>my</em> control." Here is that decision as a table.</p>
<table>
<thead>
<tr>
<th>Topology</th>
<th>Who owns the client-edge WebRTC leg</th>
<th>TURN needed?</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A. Direct browser → OpenAI</strong></td>
<td>OpenAI (public endpoint, host-only)</td>
<td><strong>No</strong> — and you can't add it</td>
<td>OpenAI's own TCP/443 handles restrictive networks. Simplest path.</td>
</tr>
<tr>
<td><strong>B. Browser → your media server → OpenAI</strong></td>
<td><strong>You</strong></td>
<td><strong>Yes</strong> — STUN + TURN, ideally TURNS/443</td>
<td>The dominant production pattern. Enterprise networks fail without TURN.</td>
</tr>
<tr>
<td><strong>C. Python/</strong><code>aiortc</code> <strong>agent ↔ browsers</strong></td>
<td><strong>You</strong> (the <code>aiortc</code> endpoint)</td>
<td><strong>Yes</strong>, on the browser-facing leg</td>
<td>Server-side WebRTC in Python; same NAT rules apply.</td>
</tr>
<tr>
<td><strong>D. Telephony / SIP → OpenAI SIP</strong></td>
<td>Your SBC/gateway (SIP, not WebRTC)</td>
<td>N/A for SIP</td>
<td>TURN reappears only if a WebRTC softphone leg exists.</td>
</tr>
<tr>
<td><strong>E. Server already has the audio → OpenAI WebSocket</strong></td>
<td>Nobody (no browser leg)</td>
<td><strong>No</strong></td>
<td>WebSocket, no ICE at all.</td>
</tr>
</tbody></table>
<h2>The Python and server-side path</h2>
<p>Python builders hit this split constantly, so it deserves its own section. There are two very different Python paths, and only one of them touches ICE.</p>
<p>If your server <strong>already has the audio</strong> — from a telephony system, a media pipeline, or a worker — use the WebSocket transport with the <code>openai</code> Python SDK. There is no browser peer, no ICE, and no TURN.</p>
<p>If your Python service must <strong>be</strong> a WebRTC peer — for example a headless agent that browsers connect to directly — you use <code>aiortc</code>, "WebRTC and ORTC implementation for Python using asyncio" (<a href="https://github.com/aiortc/aiortc">aiortc</a>, accessed 2026-07-17). Now you own the browser-facing leg, and you are back in topology C: STUN and TURN required.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/n7mir6oiowcec8dgsbj5.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Configuring ICE in <code>aiortc</code> is a standard <code>RTCConfiguration</code>. Point it at your relay, preferring TURNS on 443 for locked-down networks:</p>
<pre><code class="language-python">from aiortc import RTCConfiguration, RTCIceServer, RTCPeerConnection

config = RTCConfiguration(iceServers=[
    RTCIceServer(urls="stun:openrelay.metered.ca:80"),
    RTCIceServer(
        urls="turns:openrelay.metered.ca:443?transport=tcp",
        username="&lt;from your TURN credential API&gt;",
        credential="&lt;short-lived secret&gt;",
    ),
])
pc = RTCPeerConnection(configuration=config)  # your browser-facing peer now has a relay
</code></pre>
<p>This is exactly Metered Python SDK fits. <a href="https://pypi.org/project/metered-realtime/"><code>metered-realtime</code></a> (PyPI v1.0.0, async, built on <code>aiortc</code>) is the SDK for building that browser-facing WebRTC leg in Python, and it <strong>auto-injects Open Relay TURN</strong> so a Python agent that peers with browsers gets NAT traversal without you standing up coturn.</p>
<p>To be precise about what it is: <code>metered-realtime</code> is the transport layer <em>under</em> your agent, not an OpenAI Realtime client. Your agent still talks to OpenAI over WebSocket or WebRTC; <code>metered-realtime</code> handles the browser-facing WebRTC peer and its relay.</p>
<h2>Why your voice agent fails on office and hospital Wi-Fi</h2>
<p>This is the failure that many devs face, and it maps exactly onto the topology table. When a <a href="https://developers.openai.com/api/docs/guides/realtime">WebRTC AI</a> agent "works on my machine" but dies on a customer's corporate network, the broken leg is almost always the one you own.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/4n8l89tjq91itlra5ta4.png" alt="Image description" style="display:block;margin:0 auto" />

<p>Corporate, hospital, and bank networks block outbound UDP and non-standard ports, and many run deep packet inspection that drops traffic on 443 that is not genuine TLS. Symmetric NAT breaks the direct peer path on top of that. Your host and server-reflexive candidates all fail, and the call never connects.</p>
<p>TURN over TLS on port 443 is the escape hatch. It performs a real TLS handshake and looks identical to an HTTPS request, so it survives both the firewall and the DPI. For a deeper treatment of why 443 and TURNS specifically are what get through. you can test the TURN over TLS in TURN server testing tools like: <a href="https://www.metered.ca/turn-server-testing">TURN server testing</a></p>
<p>Latency matters here too, because this is voice. A relay three regions away adds audible delay, so a production TURN service with relays near your users — not a single box — is what keeps relayed calls sounding real. This is the same relay reality behind every <a href="https://dev.to/alakkadshaw/turn-for-ai-voice-agents-why-agent-traffic-is-almost-100-relay-3meo">TURN server for AI agents</a></p>
<p>agent audio is real-time media, and real-time media behind a corporate firewall needs a good relay. And when a relayed call still drops mid-session — networks change, Wi-Fi roams — WebRTC reconnection handling is what gets the user back without a page refresh.</p>
<p>The fix has two speeds. <a href="https://www.metered.ca/tools/openrelay/">Open Relay</a> gives you 20 GB/month of free TURN with ports 80, 443, and TURNS out of the box</p>
<blockquote>
<p><strong>METERED TURN — for the leg you own (facts dated 2026-07-17)</strong></p>
<ul>
<li><strong>Metered TURN product:</strong> 500 MB free trial, then paid.</li>
</ul>
<p><strong>Production tiers</strong> (<a href="https://metered.ca/stun-turn">https://metered.ca/stun-turn</a>, verified 2026-07-03)</p>
<ul>
<li><p>Growth <strong>$99 / 150 GB</strong>, Business <strong>$199 / 500 GB</strong>, Enterprise <strong>$499 / 2 TB</strong>, custom above. Usage is metered as <strong>ingress + egress</strong>.</p>
</li>
<li><p><strong>31+ regions, 100+ edge PoPs</strong> for low-latency relayed voice.</p>
</li>
<li><p>Ports <strong>80 / 443 / TURNS</strong>, dynamic per-session credentials, per-credential analytics, 24/7 human support.</p>
</li>
</ul>
</blockquote>
<p><a href="https://metered.ca/stun-turn">Metered's managed TURN service</a> is the same relay function across 31+ regions with fixed, allowlistable IPs and region pinning — the connectivity most enterprise voice deployments end up needing.</p>
<h2>What OpenAI Realtime costs</h2>
<p>Cost is the other thing that surprises builders, so here are the current list prices. These are OpenAI's published figures per 1M tokens unless noted (developers.openai.com pricing, accessed 2026-07-17).</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/whm2p5kxgbj1oif8ivtd.png" alt="Image description" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th>Model</th>
<th>Audio in</th>
<th>Audio out</th>
<th>Text in / out</th>
</tr>
</thead>
<tbody><tr>
<td><code>gpt-realtime-2.1</code></td>
<td><strong>$32.00</strong></td>
<td><strong>$64.00</strong></td>
<td>\(4.00 / \)24.00</td>
</tr>
<tr>
<td><code>gpt-realtime-2.1-mini</code></td>
<td><strong>$10.00</strong></td>
<td><strong>$20.00</strong></td>
<td>\(0.60 / \)2.40</td>
</tr>
<tr>
<td><code>gpt-realtime-translate</code></td>
<td>—</td>
<td>—</td>
<td><strong>$0.034 / minute</strong></td>
</tr>
<tr>
<td><code>gpt-realtime-whisper</code></td>
<td>—</td>
<td>—</td>
<td><strong>$0.017 / minute</strong></td>
</tr>
</tbody></table>
<p>For per-minute intuition, user audio runs roughly 600 tokens per minute and assistant audio roughly 1,200 tokens per minute. Independent measurements suggest a typical agent costs around <strong>$0.18–$0.46 per minute uncached</strong>, dropping to roughly <strong>$0.04–$0.10 per minute</strong> with prompt caching, trimmed tool outputs, and server-side VAD (third-party 2026 measured-session write-ups, accessed 2026-07-17).</p>
<p>Treat those per-minute figures as independent estimates, not OpenAI's own numbers — methodology varies. The list prices above are the facts; the per-minute ranges are directional.</p>
<h2>Putting it together: a reference architecture</h2>
<p>Stack the pieces and the production shape is clear. A browser captures audio and connects over a WebRTC leg to your backend; your backend runs VAD, tools, and guardrails, then talks to OpenAI; and a TURN relay sits on the browser-facing leg for the users who need it.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6gow5klzo4u2doj0qbc3.png" alt="Image description" style="display:block;margin:0 auto" />

<p>You own two things in that picture that OpenAI does not give you: the browser-facing WebRTC leg (which needs TURN) and the signalling for it. If you would rather not wire the backend leg yourself, our free, open-source SDK <strong>LLMRTC</strong> (<code>@llmrtc/llmrtc-core</code>, <code>-backend</code>, <code>-web-client</code>; Apache 2.0) is a batteries-included version of this backend — browser ⇄ WebRTC ⇄ Node backend ⇄ providers.</p>
<p>LLMRTC is provider-agnostic and lists OpenAI among its supported providers — its <code>OpenAILLMProvider</code>, <code>OpenAIWhisperProvider</code>, and <code>OpenAITTSProvider</code> are swappable by config (llmrtc.org, accessed 2026-07-17) — and its own docs recommend Open Relay TURN for production. Built by our team, it is the "don't hand-roll the media backend" option for a <a href="https://dev.to/alakkadshaw/build-an-ai-voice-agent-in-typescript-cloud-or-100-local-one-config-swap-40f1">build AI voice agent</a> project.</p>
<p>One more piece you own: signalling for that browser-to-server leg. If you build it yourself, Metered Realtime is free managed signalling with an MIT-licensed open-source client, so you can <a href="https://www.metered.ca/tools/openrelay/webrtc-signaling-server">start free</a> instead of standing up your own WebSocket layer. It is the natural companion to the relay — the two things OpenAI's direct path handles for you, and you handle yourself the moment you own a leg.</p>
<p>That is the whole architecture in one honest sentence: OpenAI gives you the model and a public endpoint; you give yourself the media leg, its relay, and its signalling — and TURN lives on that leg, not on OpenAI's.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/aoqwychx60k9ww2bobe6.png" alt="Image description" style="display:block;margin:0 auto" />

<h2>Frequently asked questions</h2>
<h3>Does the OpenAI Realtime API need a TURN server?</h3>
<p>Not on the direct browser-to-OpenAI path. OpenAI terminates WebRTC at public endpoints with host candidates only and a TCP/443 fallback, so that leg traverses most networks without your TURN — and you cannot add one (webrtcHacks, 2025-09-23). You need TURN the moment you own a WebRTC leg, such as browser to your media server, where users on restrictive networks fail without a relay.</p>
<h3>WebRTC or WebSocket for the OpenAI Realtime API?</h3>
<p>Use WebRTC for browser and mobile clients that capture or play audio directly, and WebSocket when your server already has raw audio from a media pipeline, call system, or worker (OpenAI guidance, accessed 2026-07-17). WebRTC is the client-edge transport and involves ICE; WebSocket is the server-side transport with no ICE and no TURN.</p>
<h3>Why does my OpenAI Realtime WebRTC agent fail on a corporate network?</h3>
<p>Because the failing leg is one you own, not the OpenAI leg. Corporate, hospital, and bank networks block UDP and non-standard ports and inspect port 443, so your browser-to-your-server WebRTC connection cannot use host or server-reflexive candidates. TURN over TLS on port 443 is the fix — it looks like ordinary HTTPS and survives deep packet inspection.</p>
<h3>How do I connect to the OpenAI Realtime API from Python?</h3>
<p>Two ways. If your server already has the audio, use the <code>openai</code> Python SDK over WebSocket — no ICE, no TURN. If your Python service must be a WebRTC peer that browsers connect to, use <code>aiortc</code> with an <code>RTCConfiguration</code> that includes STUN and TURN ICE servers, because you now own NAT traversal on the browser-facing leg.</p>
<h3>Is the OpenAI Realtime API generally available, and which model should I use?</h3>
<p>Yes. It reached GA on 2025-08-28 with <code>gpt-realtime</code> (OpenAI, accessed 2026-07-17). As of 2026-07-17 the catalog lists <code>gpt-realtime-2</code> as default with <code>gpt-realtime-2.1</code> and <code>gpt-realtime-2.1-mini</code> as current point releases. Pin a specific model and date it, because OpenAI ships new realtime models every few months.</p>
<h3>How much does the OpenAI Realtime API cost per minute?</h3>
<p>OpenAI prices <code>gpt-realtime-2.1</code> at \(32 per 1M audio-input tokens and \)64 per 1M audio-output tokens, with the mini at $10 and $20 (developers.openai.com, accessed 2026-07-17). Independent 2026 measurements suggest roughly $0.18–$0.46 per minute uncached, falling to about $0.04–$0.10 with caching and trimmed outputs — estimates, not OpenAI figures.</p>
<h2>The bottom line</h2>
<p>The OpenAI Realtime API over WebRTC does not need a TURN server on the direct path — OpenAI built the firewall escape hatch into its own public endpoints, and you cannot add your own relay there. That is the part existing guides simply do not cover.</p>
<p>But production voice agents put a server in the loop, and that creates a WebRTC leg you own. On that leg, users behind symmetric NAT and UDP-blocked enterprise networks fail without TURN over TLS on port 443 — the same connectivity problem every serious WebRTC app eventually meets.</p>
<p>So build the direct path when you can, and the moment you own a media leg, put a real relay under it: <a href="https://www.metered.ca/tools/openrelay/">free on Open Relay</a>, or move to <a href="https://www.metered.ca/stun-turn">Metered's managed TURN service</a> when you need 31+ regions, fixed IPs, and per-session credentials for relayed voice that actually connects.</p>
<hr />
<p><strong>About the author:</strong> This guide was written by James Bordane an Open Source enthusiast</p>
]]></content:encoded></item><item><title><![CDATA[TURN for AI Voice Agents: When Your Agent Needs a Relay — and When It Doesn't]]></title><description><![CDATA[This article was originally published here: TURN for AI voice agents
Does an AI voice agent need a TURN server? Usually yes — and the precise reason matters more than the slogan.
A voice agent connect]]></description><link>https://bordane.hashnode.dev/turn-for-ai-voice-agents-when-your-agent-needs-a-relay-and-when-it-doesn-t</link><guid isPermaLink="true">https://bordane.hashnode.dev/turn-for-ai-voice-agents-when-your-agent-needs-a-relay-and-when-it-doesn-t</guid><category><![CDATA[AI]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[WebRTC]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[James bordane]]></dc:creator><pubDate>Fri, 17 Jul 2026 16:01:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63af01a3359f463d43abebd0/39526258-20bc-4239-8551-3580bf8a3626.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This article was originally published here:</em> <a href="https://medium.com/@jamesbordane57/turn-for-ai-voice-agents-why-agent-traffic-is-almost-100-relay-c3dbcf072077"><em>TURN for AI voice agents</em></a></p>
<p><strong>Does an AI voice agent need a TURN server?</strong> Usually yes — and the precise reason matters more than the slogan.</p>
<p>A voice agent connects a user's browser to a model in the cloud over WebRTC. Many users connect directly, but anyone on a corporate network that blocks UDP can only reach the agent through a relay on TCP port 443.</p>
<p>That block is per-network, not per-session — so for those users, a <strong>TURN server for AI agents</strong> isn't optional. Their media is 100% relay-dependent.</p>
<p>That's the honest version of a claim you've probably seen stated as a flat absolute. Let's fix the absolute, then show you exactly when — and how — to wire the relay in.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9pj5c6in8oi9q4ba8od3.png" alt="Image description" style="display:block;margin:0 auto" />

<blockquote>
<p><strong>TL;DR:</strong> AI voice agent media travels browser-to-cloud over WebRTC, not peer-to-peer, so when the direct path is blocked a relay is the only fallback. Most home users connect directly, but corporate firewalls that block UDP force every session onto a TURN relay over TCP/443, and some platforms (AWS Bedrock AgentCore) mandate TURN outright.</p>
</blockquote>
<h2>How an AI voice agent actually connects</h2>
<p>Start with the shape of the connection, because everything downstream follows from it.</p>
<p>A voice agent is not a peer-to-peer call. It's a browser talking to a model running on a server in the cloud, over a single WebRTC connection. Your microphone audio flows up; the agent's synthesized voice streams back down.</p>
<p>That means there is exactly <strong>one remote endpoint</strong> — the cloud — and exactly <strong>one non-direct option</strong> if the direct path fails: a relay.</p>
<p>Compare that to a human-to-human call, where two peers can sometimes find a local network path to each other, or fall back through a relay if not. An agent has no second peer to try. There's no LAN-local shortcut, no alternate route — just the cloud endpoint and whatever path that can reach it.</p>
<p>So the relay isn't a nice-to-have you bolt on for edge cases. It's the only insurance the architecture leaves you when the direct path is blocked.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/qs5ststkpaplyv31mmhz.png" alt="Image description" style="display:block;margin:0 auto" />

<h2>How much agent traffic actually needs a relay?</h2>
<p>It depends on the network — and that dependence is the whole answer. Relay usage varies widely: some users never touch a relay, others can't connect without one.</p>
<p>The cloud agent has a <strong>public IP</strong>, so a user on an open home connection usually reaches it <strong>directly</strong> — no relay involved at all. Some vendor marketing rounds this up to an absolute, but that skips the users who connect straight through.</p>
<h2>The three cases where a relay is the only path</h2>
<p>Here's the claim worth carrying. Not "all agent traffic is 100% relay" — instead, three specific, defensible cases where a voice agent's media is fully relay-dependent, and a fourth reality that ties them together.</p>
<p><strong>One: no second peer, no shortcut.</strong> Because the agent has a single cloud endpoint, a relay is the only non-direct path. When the direct route fails, there is nothing else to try</p>
<p><strong>Two: UDP-blocking is binary per network.</strong> Most home users on open UDP connect straight to the agent — no TURN needed. But a locked-down corporate or enterprise LAN blocks UDP entirely and allows outbound traffic on only a few ports. There, TURN over TLS on port 443 "is often the only path that gets through, because that port looks like ordinary HTTPS traffic". For that population it isn't 15% — it's effectively 100%, because the block is per-network, not per-session.</p>
<p><strong>Three: some platforms mandate TURN by architecture.</strong> AWS Bedrock AgentCore's WebRTC runtime states flatly that "TURN relay is required for media traffic between the client and the agent," and offers Amazon KVS as managed TURN (<a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-webrtc.html">AWS docs, 2026-03</a>). On those platforms, TURN is part of the connection path regardless of the user's network.</p>
<p><strong>And the fourth reality:</strong> because you can't predict which of your users sits behind a UDP-blocking firewall or a carrier-grade NAT, you provision the relay for all of them. The agent that "works in the demo" is the one that skipped this step and hasn't met a corporate user yet.</p>
<h2>The pattern we see: works on your network, breaks on mobile and in the office</h2>
<p>A developer's voice agent works perfectly on their local network. Then they connect a device over a mobile phone, or from inside an office network — and it fails. Audio never arrives, even though the app says "connected."</p>
<p>Nearly every time, the cause is the same: TURN wasn't configured properly. Once we walk the team through a correct TURN setup, the agent works everywhere — on mobile networks as well as inside offices.</p>
<p>Two mechanisms sit behind that one symptom. <strong>Mobile networks</strong> typically run carrier-grade NAT (CGNAT), which behaves like symmetric NAT and makes the address STUN discovers unusable to the far side. <strong>Office and campus Wi-Fi</strong> blocks UDP outright at the firewall.</p>
<p>Different mechanism, same result: the direct path dies, and only a relay on TCP/443 survives.</p>
<p>The reason this is so easy to miss is that your development machine is the one environment where none of it applies. On localhost there's no network to cross. Ship to real users on real networks, and the relay is suddenly load-bearing.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kpmcqbg6ne7jfnsa3mul.png" alt="Image description" style="display:block;margin:0 auto" />

<h2>TURN requirement by deployment scenario (2026)</h2>
<p>Rather than argue percentages, here's the decision laid out by scenario. Each row is dated and sourced, so you can map your own deployment to a row and know where you stand.</p>
<p><strong>TURN requirement by deployment scenario — verified 2026-07-15:</strong></p>
<table>
<thead>
<tr>
<th>Deployment scenario</th>
<th>Direct path works?</th>
<th>TURN relay needed?</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Home / local residential user, UDP open</td>
<td>Often no</td>
<td>Frequently</td>
<td>STUN-assisted direct path to the agent's public IP is sometimes available</td>
</tr>
<tr>
<td>Mobile / carrier-grade NAT</td>
<td>Often no</td>
<td>Frequently</td>
<td>CGNAT behaves like symmetric NAT and blocks inbound UDP</td>
</tr>
<tr>
<td><strong>Corporate / enterprise, UDP blocked</strong></td>
<td><strong>No</strong></td>
<td><strong>Yes — effectively 100%</strong></td>
<td>Only TCP/443 escapes; TURN over TLS is the sole path</td>
</tr>
<tr>
<td>Symmetric NAT (either side)</td>
<td>No</td>
<td>Yes</td>
<td>STUN-discovered address is unusable to the far side</td>
</tr>
<tr>
<td>AWS Bedrock AgentCore (KVS)</td>
<td>—</td>
<td><strong>Required by platform</strong></td>
<td>Docs state that TURN relay is required for media traffic between the client and the agent</td>
</tr>
<tr>
<td>OpenAI hosted Realtime</td>
<td>Yes, handled</td>
<td>Handled by OpenAI</td>
<td>Private ICE-TCP relay-transceiver, not classic TURN</td>
</tr>
<tr>
<td>Self-hosted framework ( aiortc / generic)</td>
<td>No</td>
<td><strong>Yes — you add it</strong></td>
<td>No hyperscaler relay network; TURN over port 443 provides firewall traversal</td>
</tr>
</tbody></table>
<p>Read down the "TURN relay needed?" column and the pattern is obvious. The only rows where you can skip a relay are the open-home case and the hosted platforms that quietly run their own relay for you. Every self-hosted or enterprise-facing deployment lands on "yes."</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/q9uyr0y9wcnclsbkjzkz.png" alt="Image description" style="display:block;margin:0 auto" />

<h2>How to wire TURN into your voice agent</h2>
<p>The mechanism, then the config. WebRTC gathers connection candidates, and you hand it a list of ICE servers to try: STUN discovers your public address, and TURN relays your media when a direct path is impossible. If those three acronyms are fuzzy, this <a href="https://dev.to/aprogrammer22/stun-vs-turn-vs-ice-the-webrtc-networking-explained-4jpn">STUN vs TURN vs ICE explainer</a> is a clean primer.</p>
<p>For a voice agent, you pass an <code>iceServers</code> array into your peer connection — the same shape whether you use raw WebRTC or a framework. A minimal config includes a STUN entry and a TURN entry with credentials:</p>
<pre><code class="language-javascript">const iceServers = [
  { urls: "stun:&lt;your-stun-url&gt;" },
  {
    urls: [
      "turn:&lt;your-turn-url&gt;:443?transport=tcp",  // TCP/443 survives UDP-blocking firewalls
      "turns:&lt;your-turn-url&gt;:443",               // TURN over TLS, looks like HTTPS
    ],
    username: "&lt;short-lived-username&gt;",
    credential: "&lt;short-lived-credential&gt;",
  },
];

const pc = new RTCPeerConnection({ iceServers });
</code></pre>
<p>Two details do the heavy lifting. The <code>transport=tcp</code> on port 443 is the entry that gets through corporate firewalls, and the <code>turns:</code> (TURN over TLS) entry makes that traffic look like ordinary HTTPS. Serve both, and the strict-network users from the scenario table can finally connect.</p>
<p>For debugging, one setting is worth knowing: <code>iceTransportPolicy: "relay"</code> forces every candidate through TURN. Set it during testing to confirm your relay path works before real firewalls are in the picture — if it connects with <code>relay</code> forced, your locked-down users will connect too.</p>
<p>There's a second piece your agent needs, and it's easy to forget in the media excitement: a <strong>signalling channel</strong> to exchange those SDP offers and ICE candidates in the first place. That's the "and how do the two sides find each other?" question. <a href="https://www.metered.ca/tools/openrelay/webrtc-signaling-server/">Metered Realtime</a> provides managed signalling free — 100 concurrent connections and 100,000 messages a month, with an MIT-licensed open-source client — so the same vendor covering your relay can cover the control channel too, without a second integration.</p>
<p>If you'd rather see this end to end, we built and measured a full TypeScript voice agent — mic to model and back, with the TURN step wired in — in <a href="https://dev.to/alakkadshaw/build-an-ai-voice-agent-in-typescript-cloud-or-100-local-one-config-swap-40f1">this build-a-voice-agent walkthrough</a>. It's the build-side companion to this infrastructure piece.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ukeaf2op21k4dt1r8h5w.png" alt="Image description" style="display:block;margin:0 auto" />

<h2>TURN for self-hosted LiveKit Agents</h2>
<p>If you're running <strong>LiveKit Agents</strong> self-hosted, the NAT-and-firewall reality is exactly the same — your media crosses the same hostile networks as any other WebRTC app, and remote users behind UDP-blocking firewalls need a relay to reach your deployment.</p>
<p>LiveKit's server can run an embedded TURN service, but many teams point a self-hosted deployment at an <strong>external, multi-region TURN service</strong> for production reach and redundancy. You do this by supplying external TURN URLs and short-lived credentials in the server's ICE/TURN configuration, so every client LiveKit provisions receives relay candidates on TCP/443 alongside the usual STUN and UDP options.</p>
<p>The wiring is the same principle as the <code>iceServers</code> block above — a STUN entry plus a <code>turn:</code>/<code>turns:</code> entry on port 443 — just applied at the LiveKit-server layer instead of per peer connection. Point it at a relay with broad regional coverage and 24/7 support, and your self-hosted LiveKit Agents deployment inherits the enterprise-firewall traversal it needs.</p>
<p>Both <a href="https://www.metered.ca/stun-turn">Metered's TURN service</a> and the free <a href="https://www.metered.ca/tools/openrelay/">Open Relay</a> network slot in here as that external TURN target.</p>
<h2>TURN is table stakes — the honest bottom line</h2>
<p>Here's the position we'll stake our name on, after a decade of running relays for other people's WebRTC.</p>
<p>TURN is table stakes when you're working with WebRTC. Many peer-to-peer connections simply don't hold up in the real world — especially when it matters most — because of NAT and firewall rules.</p>
<p>Mobile networks sit behind CGNAT. The Wi-Fi inside hospitals, schools, and offices blocks the direct path. Those are precisely the places your agent will be used.</p>
<p>So the pragmatic move isn't to debug the failing 15% after launch. It's to provision the relay from day one, offer it on TCP/443 with TLS, and mint short-lived credentials — then stop thinking about NAT and ship. The relay is cheap insurance against the exact users you most want to impress.</p>
<p>"100% relay" was never the right way to say it. "You cannot ship a production voice agent without a relay for the users whose direct path is blocked" — that's the truth, and it's enough.</p>
<h2>Frequently Asked Questions</h2>
<h3>How much AI voice-agent traffic actually needs a TURN relay?</h3>
<p>It varies widely by network, so no single percentage fits. Users on open home connections often reach the agent's public-IP server directly, with no relay at all.</p>
<p>On UDP-blocking corporate networks, every session needs a TURN relay over TCP/443, because that's the only path out. Some platforms also mandate TURN regardless of the user's network.</p>
<h3>Does the OpenAI Realtime API need a TURN server?</h3>
<p>OpenAI's hosted Realtime service handles connectivity itself — it engineered a private relay-transceiver over ICE-TCP and skips classic TURN (<a href="https://www.infoq.com/news/2026/05/openai-voice-ai-scale/">InfoQ, 2026-05-20</a>). But if you self-host the agent instead of using OpenAI's endpoint, you don't have their global relay network. You add a TURN server on 443 to get the same firewall traversal for your own users.</p>
<h3>Why does my voice agent work locally but fail for real users?</h3>
<p>On localhost there's no network to cross, so WebRTC connects trivially. Real users sit behind NATs and corporate firewalls that block direct UDP — mobile CGNAT and office Wi-Fi are the usual culprits. Without a TURN relay carrying media over TCP/443, the connection has nowhere to go, and audio never reaches the agent despite a "connected" status.</p>
<h3>Why does WebRTC fail on corporate networks?</h3>
<p>Corporate firewalls commonly block UDP and allow outbound traffic on only a few ports. WebRTC's default UDP media path can't get out, so the connection fails silently. The fix is a TURN server offering TURN over TLS on port 443, which looks like ordinary HTTPS traffic and passes straight through the firewall</p>
<h3>Do managed agent platforms require TURN?</h3>
<p>Some do, by architecture. AWS Bedrock AgentCore's WebRTC runtime states that TURN relay is required for media between the client and the agent, and offers Amazon KVS as managed TURN (<a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-webrtc.html">AWS docs, 2026-03</a>). On those platforms TURN isn't optional — it's part of the connection path regardless of the user's network.</p>
<h3>coturn or managed TURN for a voice agent?</h3>
<p>coturn is free software, but you own TLS certificates, credential rotation, bandwidth, DDoS exposure, and patching across regions. Managed TURN handles all of it with global coverage and an SLA. Self-host if you have the ops capacity and volume; use a managed relay to ship reliably without running relay infrastructure yourself.</p>
<h2>Getting started with Metered TURN for voice agents</h2>
<p>If you'd rather not run relays across a dozen regions, <a href="https://www.metered.ca/stun-turn">Metered's TURN service</a> is built for exactly the deployment reality above.</p>
<p>You get 31+ regions and 100+ edge PoPs, TURN on ports 80 and 443 with TURNS/TLS, dynamic short-lived credentials, and 24/7 human support from a team that has operated production TURN, STUN, and signalling for a decade.</p>
<hr />
<p><strong>About the author:</strong> This guide was written by James Bordane, a developer, and open source enthusiast and network engineer</p>
]]></content:encoded></item><item><title><![CDATA[Coturn Alternative: How to Migrate from Self-Hosted Coturn to a Managed TURN Service]]></title><description><![CDATA[If you're running coturn in production, you already know the routine. TLS certificate renewals, capacity planning for traffic spikes, debugging relay failures at 2 AM, and patching CVEs that drop with zero warning. Your senior engineers are spending ...]]></description><link>https://bordane.hashnode.dev/coturn-alternative-how-to-migrate-from-self-hosted-coturn-to-a-managed-turn-service</link><guid isPermaLink="true">https://bordane.hashnode.dev/coturn-alternative-how-to-migrate-from-self-hosted-coturn-to-a-managed-turn-service</guid><category><![CDATA[Web Development]]></category><category><![CDATA[WebRTC]]></category><category><![CDATA[software development]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[James bordane]]></dc:creator><pubDate>Sun, 01 Feb 2026 19:28:42 GMT</pubDate><content:encoded><![CDATA[<p>If you're running coturn in production, you already know the routine. TLS certificate renewals, capacity planning for traffic spikes, debugging relay failures at 2 AM, and patching CVEs that drop with zero warning. Your senior engineers are spending 15–20 hours a month maintaining TURN infrastructure that isn't your product.</p>
<p>There's a better path. Migrating from self-hosted coturn to a managed TURN service eliminates the operational burden entirely. And the switch is simpler than most teams expect — TURN servers are loosely coupled to your application, so the migration requires changing just a URL and credentials.</p>
<p>This guide covers everything you need to make the move. You'll learn why teams are seeking a coturn alternative, what managed services are available, how to evaluate them, and how to execute the migration step by step.</p>
<h2 id="heading-why-teams-migrate-away-from-coturn">Why teams migrate away from coturn</h2>
<p>Coturn is the de facto open-source TURN server. With 13,500+ GitHub stars and widespread adoption across projects like Jitsi, Nextcloud Talk, and Matrix, it has been the default choice for self-hosted TURN infrastructure for years.</p>
<p>But popularity doesn't mean it's the right choice for every team today. Here's why a growing number of engineering organizations are searching for a coturn alternative.</p>
<h3 id="heading-the-maintenance-burden-is-real">The maintenance burden is real</h3>
<p>Running coturn across multiple regions means you own every piece of the stack. OS patching, TLS certificate rotation, DDoS mitigation, capacity planning, monitoring, and on-call — all of it falls on your team.</p>
<p>In practice, this translates to 15–20 hours per month of senior engineering time per deployment. At senior WebRTC engineer salaries ($180–250K/year), that's roughly $36–50K per year in opportunity cost — time your team could spend building features that drive revenue.</p>
<p>The operational surface area is significant:</p>
<ul>
<li><p><strong>Multi-region deployment</strong>: Each region is a separate instance to provision, configure, and maintain</p>
</li>
<li><p><strong>Credential management</strong>: No built-in API for credential rotation or expiry</p>
</li>
<li><p><strong>Auto-scaling</strong>: Coturn doesn't scale automatically. Traffic spikes require manual intervention</p>
</li>
<li><p><strong>Monitoring and alerting</strong>: You need to build or integrate your own observability stack</p>
</li>
<li><p><strong>DDoS protection</strong>: Public-facing TURN endpoints are frequent targets</p>
</li>
</ul>
<h3 id="heading-security-vulnerabilities-keep-surfacing">Security vulnerabilities keep surfacing</h3>
<p>In December 2025, CVE-2025-69217 disclosed a serious vulnerability in coturn. Versions 4.6.2r5 through 4.7.0-r4 used libc's <code>random()</code> function instead of OpenSSL's <code>RAND_bytes</code> for generating nonces and randomizing ports.</p>
<p>The result? An attacker could predict nonces with roughly 50 sequential unauthenticated allocation requests, enabling authentication spoofing and port prediction.</p>
<p>Coturn v4.8.0 (released January 2026) patched this CVE. But when you self-host, you are responsible for applying the patch. Every hour between disclosure and deployment is a window of exposure.</p>
<p>This isn't a one-time issue. Coturn's CVE history includes:</p>
<ul>
<li><p><strong>CVE-2020-26262</strong>: Loopback address bypass</p>
</li>
<li><p><strong>CVE-2020-4067</strong>: Information leak via uninitialized buffer</p>
</li>
<li><p><strong>Pre-4.5.0.9</strong>: SQL injection in the admin web portal</p>
</li>
</ul>
<h3 id="heading-sustainability-concerns-persist">Sustainability concerns persist</h3>
<p>Coturn's maintenance history has been uneven. A widely cited 2022 analysis points to periods of inactivity, hundreds of open issues, and unmerged pull requests.</p>
<p>The project has seen renewed activity — v4.8.0 is a meaningful release with DDoS handling improvements and memory leak fixes. The repository now shows 143 contributors and 1,832 total commits.</p>
<p>But 343 open issues remain. And the project has no corporate backing or dedicated full-time maintainer. For teams building mission-critical applications, the question isn't whether coturn works today. It's whether you can depend on it for years of continuous operation without a guaranteed support structure.</p>
<h2 id="heading-the-case-for-managed-turn-services">The case for managed TURN services</h2>
<p>When teams evaluate a coturn alternative, the decision often comes down to: <strong>do you want to operate TURN infrastructure, or do you want TURN infrastructure that operates itself?</strong></p>
<h3 id="heading-what-you-stop-doing">What you stop doing</h3>
<p>The moment you migrate from coturn to a managed service, your team stops:</p>
<ul>
<li><p>Provisioning and configuring servers across regions</p>
</li>
<li><p>Managing TLS certificates and protocol configurations</p>
</li>
<li><p>Building custom credential rotation tooling</p>
</li>
<li><p>Monitoring server health and setting up alerting</p>
</li>
<li><p>Handling DDoS mitigation</p>
</li>
<li><p>Debugging relay failures at 2 AM</p>
</li>
<li><p>Planning capacity for traffic spikes</p>
</li>
<li><p>Applying security patches within hours of CVE disclosure</p>
</li>
</ul>
<h3 id="heading-what-you-gain">What you gain</h3>
<p>A managed TURN service replaces all of that with:</p>
<ul>
<li><p>A single API call to provision credentials</p>
</li>
<li><p>Global coverage across dozens of regions without deploying a single server</p>
</li>
<li><p>Automatic geo-routing that connects users to the nearest relay</p>
</li>
<li><p>Built-in scaling that handles traffic spikes without intervention</p>
</li>
<li><p>SLA-backed uptime with the provider on the hook</p>
</li>
<li><p>24/7 support from engineers who specialize in TURN infrastructure</p>
</li>
</ul>
<h2 id="heading-managed-coturn-alternatives">Managed coturn alternatives</h2>
<h3 id="heading-open-relay-project-free-turn-for-development">Open Relay Project — free TURN for development</h3>
<p>The Open Relay Project provides a free community TURN server ideal for getting started.</p>
<p><strong>What you get:</strong></p>
<ul>
<li><p>20 GB per month of free TURN relay traffic</p>
</li>
<li><p>REST API with automatic geo-routing</p>
</li>
<li><p>No credit card required</p>
</li>
<li><p>Standard TURN protocols: UDP, TCP, TLS, and DTLS</p>
</li>
</ul>
<p><strong>Best for:</strong> Development environments, hackathons, proof-of-concept builds, small hobby projects</p>
<h3 id="heading-metered-production-grade-managed-turn">Metered — production-grade managed TURN</h3>
<p>For production workloads, Metered TURN server provides enterprise-grade infrastructure.</p>
<p><strong>Infrastructure:</strong></p>
<ul>
<li><p>31+ named regions with 100+ Points of Presence across 5 continents</p>
</li>
<li><p>Sub-30ms latency from anywhere in the world</p>
</li>
<li><p>99.999% historical uptime</p>
</li>
<li><p>Private high-speed TURN backbone</p>
</li>
<li><p>Premium bandwidth with direct peering</p>
</li>
</ul>
<p><strong>Developer experience:</strong></p>
<ul>
<li><p>REST API for credential management</p>
</li>
<li><p>Real-time usage dashboard</p>
</li>
<li><p>Projects for multi-tenant organization</p>
</li>
<li><p>Webhooks for event-driven notifications</p>
</li>
<li><p>Region pinning for data residency</p>
</li>
<li><p>Custom domain support for white-label</p>
</li>
</ul>
<h2 id="heading-step-by-step-migration-guide">Step-by-step migration guide</h2>
<p>Here's the good news: TURN servers are loosely coupled to your application. Migrating is straightforward.</p>
<h3 id="heading-step-1-audit-your-current-coturn-usage">Step 1: Audit your current coturn usage</h3>
<p>Understand your current TURN footprint:</p>
<ul>
<li><p><strong>Bandwidth</strong>: How many GB/month of relay traffic?</p>
</li>
<li><p><strong>Regions</strong>: Where are your servers and users?</p>
</li>
<li><p><strong>Protocols</strong>: UDP, TCP, TLS, or DTLS?</p>
</li>
<li><p><strong>Credential model</strong>: Static or time-limited credentials?</p>
</li>
</ul>
<h3 id="heading-step-2-set-up-your-managed-turn-service">Step 2: Set up your managed TURN service</h3>
<p><strong>For Open Relay Project:</strong></p>
<ol>
<li><p>Visit openrelayproject.org</p>
</li>
<li><p>Sign up for a free API key</p>
</li>
<li><p>Note your API endpoint</p>
</li>
</ol>
<p><strong>For Metered:</strong></p>
<ol>
<li><p>Visit metered.ca/stun-turn</p>
</li>
<li><p>Create a free trial account (500 MB, no credit card)</p>
</li>
<li><p>Create a project in the dashboard</p>
</li>
<li><p>Note your API key</p>
</li>
</ol>
<h3 id="heading-step-3-update-your-ice-server-configuration">Step 3: Update your ICE server configuration</h3>
<p>This is the core of the migration. Replace your coturn credentials with managed service credentials.</p>
<p><strong>Before (self-hosted coturn):</strong></p>
<p>JavaScript</p>
<pre><code class="lang-plaintext">const iceServers = [{
  urls: 'turn:your-coturn-server.com:3478',
  username: 'username',
  credential: 'password'
}];
</code></pre>
<p><strong>After (managed service):</strong></p>
<p>JavaScript</p>
<pre><code class="lang-plaintext">// Fetch TURN credentials from managed service
const response = await fetch(
  "https://api-endpoint/credentials?apiKey=YOUR_KEY"
);
const iceServers = await response.json();

// Use in your WebRTC peer connection
const pc = new RTCPeerConnection({ iceServers });
</code></pre>
<h3 id="heading-step-4-run-a-parallel-test">Step 4: Run a parallel test</h3>
<p>Don't cut over all traffic at once:</p>
<ol>
<li><p><strong>Feature flag</strong>: Route 5-10% of connections to the managed service</p>
</li>
<li><p><strong>Monitor</strong>: Compare connection success rates and call quality</p>
</li>
<li><p><strong>Validate</strong>: Test specific scenarios (corporate firewalls, NATs, mobile networks)</p>
</li>
<li><p><strong>Ramp up</strong>: Gradually increase to 25%, 50%, 75%, then 100%</p>
</li>
</ol>
<h3 id="heading-step-5-decommission-coturn">Step 5: Decommission coturn</h3>
<p>Once validated at 100%:</p>
<ol>
<li><p>Remove coturn infrastructure</p>
</li>
<li><p>Update documentation</p>
</li>
<li><p>Reclaim on-call responsibilities</p>
</li>
<li><p>Redirect engineering time to product work</p>
</li>
</ol>
<h2 id="heading-common-migration-concerns">Common migration concerns</h2>
<p><strong>"Will latency be worse with a managed service?"</strong></p>
<p>Unlikely. Self-hosted coturn typically runs in 1-3 cloud regions. Metered operates across 31+ regions with 100+ PoPs. For users outside your self-hosted regions, latency will likely improve.</p>
<p><strong>"What about vendor lock-in?"</strong></p>
<p>TURN is a standard protocol (RFC 5766). Your application uses standard ICE configuration. Switching providers requires changing the URL and credentials—no proprietary SDK or custom protocol.</p>
<p><strong>"What if the managed service goes down?"</strong></p>
<p>Metered offers up to 99.999% uptime—less than 26 seconds of downtime per month. Compare that to your actual uptime with self-hosted coturn, including unplanned outages and maintenance windows.</p>
<h2 id="heading-frequently-asked-questions">Frequently asked questions</h2>
<p><strong>Is coturn dead?</strong></p>
<p>No. Coturn released v4.8.0 in January 2026 with meaningful improvements. But "not dead" isn't the same as "thriving." The project has 343 open issues, no corporate backing, and no full-time maintainer.</p>
<p><strong>Can I migrate without changing my application code?</strong></p>
<p>Almost. The only change is your ICE server configuration—the TURN server URL and credentials. Your WebRTC application logic, signaling server, and media handling remain untouched.</p>
<p><strong>How much bandwidth does TURN relay use?</strong></p>
<p>A one-on-one video call at 720p relays approximately 1-3 GB per hour through TURN. Audio-only calls use 100-200 MB per hour. Actual consumption depends on resolution, participants, duration, and what percentage of connections require TURN relay (typically 15-20%).</p>
<h2 id="heading-making-the-switch">Making the switch</h2>
<p>Migrating from coturn to a managed TURN service is one of the highest-leverage infrastructure decisions a WebRTC team can make. You eliminate operational burden, reduce security exposure, and gain global coverage.</p>
<p>Start with the Open Relay Project if you want to test the concept for free. When you're ready for production, visit metered.ca/stun-turn to explore full managed TURN infrastructure with 31+ regions, 99.999% uptime, and 24/7 support.</p>
<p>Your engineers have better things to build than TURN server infrastructure. Let them.</p>
]]></content:encoded></item><item><title><![CDATA[NAT Traversal: How It Works, Why It Breaks WebRTC, and How to Fix It]]></title><description><![CDATA[They'll blame your platform, not their network.
About 15-30% of WebRTC connections fail because users sit behind NATs that prevent direct peer-to-peer communication. If you’re building a video calling product, that means roughly 1 in 5 of your users ...]]></description><link>https://bordane.hashnode.dev/nat-traversal-how-it-works-why-it-breaks-webrtc-and-how-to-fix-it</link><guid isPermaLink="true">https://bordane.hashnode.dev/nat-traversal-how-it-works-why-it-breaks-webrtc-and-how-to-fix-it</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Devops]]></category><category><![CDATA[networking]]></category><category><![CDATA[software development]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[James bordane]]></dc:creator><pubDate>Sun, 01 Feb 2026 01:34:18 GMT</pubDate><content:encoded><![CDATA[<p>They'll blame your platform, not their network.</p>
<p>About 15-30% of WebRTC connections fail because users sit behind NATs that prevent direct peer-to-peer communication. If you’re building a video calling product, that means roughly 1 in 5 of your users can’t connect.</p>
<p>NAT traversal is the set of techniques that solves this problem: discovering public addresses, punching holes through NATs, and relaying traffic when all else fails.</p>
<p>This guide covers NAT traversal from first principles through production implementation. You'll learn how NATs break peer-to-peer connections, why STUN/TURN/ICE work together, why CGNAT is making the problem worse, and how to troubleshoot connection failures in production.</p>
<p>Whether you're debugging ICE candidates at 11 PM or architecting a new real-time communication product, this is the reference you'll want bookmarked.</p>
<h2 id="heading-what-is-nat-and-why-does-it-break-peer-to-peer-connections">What is NAT and why does it break peer-to-peer connections?</h2>
<p>Network Address Translation (NAT) was designed to solve a practical problem: IPv4 only provides about 4.3 billion addresses, and the internet ran out of new allocations years ago. NAT lets multiple devices on a private network share a single public IP address.</p>
<p>Your laptop, phone, and smart speaker all get private addresses (like <code>192.168.1.x</code>), and your router translates those to its single public IP when packets leave for the internet.</p>
<p>Here's how it works. When your device at <code>192.168.1.50:12345</code> sends a packet to an external server at <code>203.0.113.1:443</code>, the NAT router rewrites the source address to its own public IP and assigns a new source port -- say <code>198.51.100.1:54321</code>. It stores this mapping in a translation table.</p>
<p>When the server responds to <code>198.51.100.1:54321</code>, the NAT looks up the mapping and forwards the packet back to <code>192.168.1.50:12345</code>.</p>
<p>From the server's perspective, it's talking to the router. From your device's perspective, NAT is invisible.</p>
<p>This works well for client-server communication. The problem starts when two devices behind separate NATs try to talk directly to each other -- the exact scenario WebRTC needs for peer-to-peer calls.</p>
<p>Neither device knows the other's private address. Even if they did, private addresses aren't routable on the public internet.</p>
<p>And even if Device A somehow learns Device B's public address and port, the NAT in front of Dev/ice B will drop the incoming packet because no prior outbound packet created a mapping for that connection. The NAT has no translation table entry, so the packet is silently discarded.</p>
<p>This is the core NAT traversal problem: both sides need to send packets to create NAT mappings, but neither side can receive packets until a mapping exists.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769906319868/d16efbf6-b43b-45a6-b925-05a8cbb5c0d7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-understanding-nat-types-and-their-impact-on-connectivity">Understanding NAT types and their impact on connectivity</h2>
<p>Not all NATs behave the same way. The type of NAT a device sits behind determines whether direct peer-to-peer connections are possible.</p>
<p>Understanding these differences is critical for predicting connection success rates in your WebRTC application.</p>
<h3 id="heading-the-classic-classification-and-why-its-incomplete">The classic classification (and why it's incomplete)</h3>
<p>The original NAT classification from RFC 3489 (2003) defines four types:</p>
<ul>
<li><p><strong>Full Cone NAT</strong>: Once a mapping is created (internal IP:port to external IP:port), any external host can send packets to that external address. The most permissive type.</p>
</li>
<li><p><strong>Address-Restricted Cone NAT</strong>: Only external hosts that the internal device has previously sent a packet to (by IP) can send packets back through the mapping.</p>
</li>
<li><p><strong>Port-Restricted Cone NAT</strong>: Same as address-restricted, but also restricted by port. The external host must match both the IP and port the internal device previously contacted.</p>
</li>
<li><p><strong>Symmetric NAT</strong>: A different external port mapping is created for each unique destination. A packet sent to Server A gets external port 54321, while a packet to Server B gets external port 54322. This is the most restrictive type and the hardest to traverse.</p>
</li>
</ul>
<p>You'll still see this classification everywhere. It's useful for building intuition, but it has a significant limitation: it conflates two independent behaviors.</p>
<h3 id="heading-the-modern-classification-rfc-4787">The modern classification (RFC 4787)</h3>
<p><a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc4787">RFC 4787</a> introduced a more precise framework by separating NAT behavior into two independent dimensions:</p>
<p><strong>Mapping behavior</strong> -- how the NAT assigns external ports:</p>
<ul>
<li><p><strong>Endpoint-Independent Mapping (EIM)</strong>: The same external port is used regardless of where packets are sent. If <code>192.168.1.50:12345</code> maps to <code>198.51.100.1:54321</code> for one destination, it maps to the same external port for every destination. This is "easy NAT."</p>
</li>
<li><p><strong>Endpoint-Dependent Mapping (EDM)</strong>: A different external port is assigned per destination. This is "hard NAT" -- what the classic taxonomy calls symmetric NAT.</p>
</li>
</ul>
<p><strong>Filtering behavior</strong> -- which incoming packets the NAT accepts:</p>
<ul>
<li><p><strong>Endpoint-Independent Filtering</strong>: Accepts packets from any external source once a mapping exists.</p>
</li>
<li><p><strong>Address-Dependent Filtering</strong>: Only accepts packets from IPs the internal device has sent to.</p>
</li>
<li><p><strong>Address and Port-Dependent Filtering</strong>: Only accepts packets matching both the IP and port previously contacted.</p>
</li>
</ul>
<p>Here's why this matters for NAT traversal: a NAT with endpoint-independent mapping but address-dependent filtering (common in consumer routers) will allow UDP hole punching to work even though it's not "full cone."</p>
<p>The classic taxonomy would call this "restricted cone" and leave you guessing about traversal difficulty. The modern taxonomy tells you directly: EIM means hole punching will work; EDM means you need a relay.</p>
<h3 id="heading-why-symmetric-nat-edm-is-the-enemy-of-peer-to-peer">Why symmetric NAT (EDM) is the enemy of peer-to-peer</h3>
<p>With endpoint-independent mapping, STUN can discover your public IP:port, and that same IP:port will work for communicating with any peer. You tell your peer "send packets here," and they arrive.</p>
<p>With endpoint-dependent mapping, the port STUN discovers is only valid for talking to the STUN server. When your peer sends packets to that address, the NAT assigns a different port for the new destination -- and drops the peer's packets because they're arriving at the old port.</p>
<p>The address STUN gave you is useless for peer-to-peer communication.</p>
<p>This is why symmetric NATs are the primary reason WebRTC connections fail. And symmetric NAT behavior is common in corporate networks, mobile carriers using CGNAT, and some consumer routers.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>NAT Type (RFC 4787)</td><td>Mapping</td><td>Filtering</td><td>Hole Punch?</td><td>Prevalence</td></tr>
</thead>
<tbody>
<tr>
<td>EIM + Endpoint-Independent Filtering</td><td>Endpoint-Independent</td><td>Endpoint-Independent</td><td>Yes (easy)</td><td>Rare in practice</td></tr>
<tr>
<td>EIM + Address-Dependent Filtering</td><td>Endpoint-Independent</td><td>Address-Dependent</td><td>Yes</td><td>Common (consumer routers)</td></tr>
<tr>
<td>EIM + Address+Port-Dependent Filtering</td><td>Endpoint-Independent</td><td>Address+Port-Dependent</td><td>Yes</td><td>Common (consumer routers)</td></tr>
<tr>
<td>EDM (Symmetric)</td><td>Endpoint-Dependent</td><td>Address+Port-Dependent</td><td>No</td><td>Corporate, CGNAT, some consumer</td></tr>
</tbody>
</table>
</div><h2 id="heading-how-nat-traversal-works-the-core-techniques">How NAT traversal works: the core techniques</h2>
<p>NAT traversal is not a single protocol. It's a collection of techniques, each solving a different piece of the puzzle.</p>
<p>Here's how they work from simplest to most complex.</p>
<h3 id="heading-udp-hole-punching">UDP hole punching</h3>
<p>UDP hole punching is the most common NAT traversal technique for direct connections. It exploits a simple fact: most NATs create a mapping when an outbound packet is sent, and that mapping permits inbound packets from the destination.</p>
<p>The process works like this:</p>
<ol>
<li><p>Both peers (A and B) send their local and public address information to a signaling server (via STUN or other discovery).</p>
</li>
<li><p>The signaling server tells A about B's public address, and B about A's public address.</p>
</li>
<li><p>Both peers simultaneously send UDP packets to each other's public addresses.</p>
</li>
<li><p>When A's packet arrives at B's NAT, B's NAT may initially drop it (no mapping exists yet). But B is also sending a packet to A, which creates an outbound mapping on B's NAT.</p>
</li>
<li><p>When A's next packet arrives, B's NAT now has a mapping that permits it. The "hole" has been punched.</p>
</li>
</ol>
<p>This works reliably when both NATs use endpoint-independent mapping (EIM). Research suggests UDP hole punching succeeds 82-95% of the time across general internet traffic.</p>
<p>But when either NAT uses endpoint-dependent mapping (symmetric NAT), hole punching fails because the port the peer sends to isn't the port the NAT actually assigned for that destination.</p>
<h3 id="heading-tcp-hole-punching">TCP hole punching</h3>
<p>TCP hole punching follows the same principle but is significantly harder.</p>
<p>TCP's three-way handshake (SYN, SYN-ACK, ACK) means both sides need to send SYN packets simultaneously. If one SYN arrives before the other side has sent its own, the receiving NAT drops it as unsolicited.</p>
<p>The timing window is tight. In practice, TCP hole punching succeeds roughly 64% of the time -- substantially less reliable than UDP. This is one reason WebRTC defaults to UDP for media transport.</p>
<h3 id="heading-port-mapping-protocols-upnp-igd-nat-pmp-pcp">Port mapping protocols (UPnP IGD, NAT-PMP, PCP)</h3>
<p>A more direct approach: ask the NAT to create a mapping explicitly. Three protocols exist for this:</p>
<ul>
<li><p><strong>UPnP IGD</strong> (Universal Plug and Play Internet Gateway Device): The oldest. Widely supported but has significant security concerns -- it allows any application on the network to open ports.</p>
</li>
<li><p><strong>NAT-PMP</strong> (NAT Port Mapping Protocol): Apple's alternative, used in AirPort routers. Simpler and slightly more secure than UPnP.</p>
</li>
<li><p><strong>PCP</strong> (Port Control Protocol, <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6887">RFC 6887</a>): The modern successor to NAT-PMP. Designed to work with both IPv4 NAT and IPv6 firewalls.</p>
</li>
</ul>
<p>These protocols can create explicit port mappings, but they have a critical limitation: they only work on the first NAT hop.</p>
<p>If a user is behind CGNAT (carrier-grade NAT), UPnP/NAT-PMP/PCP can open a port on the home router, but the carrier's NAT sitting upstream is unaffected. The user is still unreachable.</p>
<h3 id="heading-relay-based-traversal">Relay-based traversal</h3>
<p>When direct connections fail -- both sides behind symmetric NATs, restrictive firewalls, or deep packet inspection -- the only option is routing traffic through an intermediary relay server.</p>
<p>Both peers connect outbound to the relay, and the relay forwards packets between them.</p>
<p>This is what TURN servers do. It adds latency (traffic takes an extra hop through the relay) and costs bandwidth (the relay provider pays for every byte), but it guarantees connectivity.</p>
<p>For production WebRTC applications, TURN is the difference between "works for 80% of users" and "works for everyone."</p>
<h2 id="heading-stun-discovering-your-public-address">STUN: discovering your public address</h2>
<p>STUN (Session Traversal Utilities for NAT, <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8489">RFC 8489</a>) is a lightweight protocol that lets a client discover its public-facing IP address and port as seen by the outside world. Think of it as asking a friend on the public internet: "What address do you see my packets coming from?"</p>
<p>The flow is straightforward:</p>
<ol>
<li><p>Your WebRTC client sends a STUN Binding Request to a STUN server on the public internet.</p>
</li>
<li><p>The request passes through your NAT, which assigns a public IP:port mapping.</p>
</li>
<li><p>The STUN server reads the source IP:port from the received packet and echoes it back in a Binding Response.</p>
</li>
<li><p>Your client now knows its public address -- the <em>server-reflexive candidate</em> in ICE terminology.</p>
</li>
</ol>
<p>STUN is fast (a single UDP round-trip), lightweight (minimal bandwidth), and free to operate at scale. Metered includes <a target="_blank" href="https://www.metered.ca/stun-turn">free STUN servers</a> on all plans.</p>
<p>But STUN has a hard limitation: it cannot help when the NAT uses endpoint-dependent mapping (symmetric NAT). The public address STUN discovers is only valid for communicating with the STUN server itself.</p>
<p>A different destination gets a different port assignment, and the STUN-discovered address becomes useless for peer-to-peer.</p>
<p>That's where TURN takes over.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769907038542/3b4e51be-7e5a-40d2-a899-95a22ce07455.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-turn-the-relay-fallback-that-ensures-100-connectivity">TURN: the relay fallback that ensures 100% connectivity</h2>
<p>STUN tells you your public address. But when that address is useless -- symmetric NATs, restrictive firewalls, CGNAT -- you need a different approach entirely.</p>
<p>TURN (Traversal Using Relays around NAT, <a target="_blank" href="https://datatracker.ietf.org/doc/rfc8656/">RFC 8656</a>) is the NAT traversal protocol of last resort -- and the most important protocol for production WebRTC. For a deeper look at what TURN does, see <a target="_blank" href="https://www.metered.ca/blog/what-is-a-turn-server-3/">what is a TURN server</a>.</p>
<p>When STUN-based hole punching fails, TURN provides a relay path. The client connects outbound to a TURN server, allocates a relay address on that server, and the TURN server forwards packets between the two peers.</p>
<p>Here's how it works:</p>
<ol>
<li><p>The client sends an Allocate Request to the TURN server, authenticated with credentials.</p>
</li>
<li><p>The TURN server allocates a relay transport address (a public IP:port on the server itself).</p>
</li>
<li><p>The client tells its peer (via signaling) to send packets to the relay address.</p>
</li>
<li><p>Both peers send traffic to the TURN server, which forwards packets between them.</p>
</li>
</ol>
<p>TURN uses a strict permission model to prevent abuse as an open relay. The client must explicitly authorize which peers can send traffic through its allocation.</p>
<h3 id="heading-the-numbers-how-often-is-turn-needed">The numbers: how often is TURN needed?</h3>
<p>Across general WebRTC traffic, 15-30% of connections require TURN relay. Chrome's internal usage metrics (UMA data) show approximately 20-25% of sessions using relay candidates.</p>
<p>The percentage varies significantly by deployment:</p>
<ul>
<li><p><strong>Consumer applications</strong> (users on home Wi-Fi): ~15-20% require TURN</p>
</li>
<li><p><strong>Mobile-heavy applications</strong> (users on carrier networks with CGNAT): ~25-35%</p>
</li>
<li><p><strong>Enterprise/corporate networks</strong> (restrictive firewalls, proxy servers): ~30-50%</p>
</li>
</ul>
<p>For a telehealth platform with patients connecting from hospitals, corporate offices, and mobile networks, the TURN requirement can hit 40% or higher.</p>
<p>Without TURN, those users simply cannot connect. Your platform looks broken, and the patient reschedules their appointment.</p>
<p>This is why TURN is not optional for production WebRTC. The question isn't whether you need TURN. It's whether you <a target="_blank" href="https://www.metered.ca/stun-turn">run it yourself or use a managed service</a>.</p>
<h3 id="heading-the-cost-of-relay">The cost of relay</h3>
<p>TURN adds latency because traffic takes an extra network hop through the relay server. It also costs bandwidth -- the relay operator pays for every byte forwarded.</p>
<p>This is why TURN is used only as a fallback, not as the default path. The ICE framework (covered next) ensures TURN is only selected when direct connections have genuinely failed.</p>
<h2 id="heading-ice-the-framework-that-ties-it-all-together">ICE: the framework that ties it all together</h2>
<p>So far we've covered individual NAT traversal techniques. ICE is what brings them together into a single, automated process.</p>
<p>Interactive Connectivity Establishment (<a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8445">RFC 8445</a>) is the framework that orchestrates NAT traversal in WebRTC. ICE doesn't replace STUN or TURN -- it uses both, along with direct connectivity checks, to find the best available path between two peers.</p>
<h3 id="heading-candidate-gathering">Candidate gathering</h3>
<p>When a WebRTC <code>RTCPeerConnection</code> starts, ICE gathers <em>candidates</em> -- potential network paths the connection could use:</p>
<ul>
<li><p><strong>Host candidates</strong>: The device's local IP addresses and ports. These work when both peers are on the same network.</p>
</li>
<li><p><strong>Server-reflexive candidates (srflx)</strong>: Public IP:port discovered via STUN. These work when NATs use endpoint-independent mapping.</p>
</li>
<li><p><strong>Relay candidates</strong>: Addresses allocated on a TURN server. These always work, at the cost of extra latency and bandwidth.</p>
</li>
<li><p><strong>Peer-reflexive candidates (prflx)</strong>: Discovered during connectivity checks when a packet arrives from an unexpected address. These represent paths that weren't predicted during gathering.</p>
</li>
</ul>
<h3 id="heading-candidate-exchange-via-signaling">Candidate exchange via signaling</h3>
<p>Once candidates are gathered, they're encoded in SDP (Session Description Protocol) and exchanged between peers through your application's signaling channel -- WebSocket, HTTP, or any other mechanism.</p>
<p>ICE doesn't define signaling; your application provides it.</p>
<p>Each candidate includes the transport address, protocol, priority, and component ID. The remote peer receives these candidates and adds them to its checklist.</p>
<h3 id="heading-connectivity-checks-and-prioritization">Connectivity checks and prioritization</h3>
<p>ICE pairs each local candidate with each remote candidate and runs connectivity checks -- essentially STUN Binding Requests sent directly between the peers. This verifies that packets can actually traverse the network path.</p>
<p>Candidate pairs are prioritized. ICE prefers:</p>
<ol>
<li><p>Host candidates (direct local connection, lowest latency)</p>
</li>
<li><p>Server-reflexive candidates (NAT-traversed direct connection)</p>
</li>
<li><p>Relay candidates (TURN, highest latency but guaranteed connectivity)</p>
</li>
</ol>
<p>The first candidate pair that succeeds becomes the nominated pair, and media flows through it. If a higher-priority pair succeeds later, ICE can switch.</p>
<h3 id="heading-ice-connection-states-to-monitor">ICE connection states to monitor</h3>
<p>In your WebRTC application, the <code>RTCPeerConnection</code> exposes ICE connection state through the <code>iceConnectionState</code> property:</p>
<ul>
<li><p><code>new</code> -- ICE agent created, no checks started</p>
</li>
<li><p><code>checking</code> -- At least one candidate pair is being tested</p>
</li>
<li><p><code>connected</code> -- A working pair is found, but checks continue for better options</p>
</li>
<li><p><code>completed</code> -- ICE has finished all checks and selected the best pair</p>
</li>
<li><p><code>failed</code> -- All candidate pairs have failed. No connectivity possible with current candidates.</p>
</li>
<li><p><code>disconnected</code> -- Connectivity was lost (network change, NAT timeout). May recover.</p>
</li>
<li><p><code>closed</code> -- ICE agent is shut down</p>
</li>
</ul>
<p>Monitoring these states is the first line of defense for diagnosing NAT traversal problems. A connection that gets stuck in <code>checking</code> or lands on <code>failed</code> is almost always a NAT/firewall issue.</p>
<h3 id="heading-webrtc-code-example-configuring-ice-with-stun-and-turn">WebRTC code example: configuring ICE with STUN and TURN</h3>
<p>Here's a practical example showing how to configure <code>RTCPeerConnection</code> with both STUN and TURN servers:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ICE server configuration with STUN and TURN</span>
<span class="hljs-keyword">const</span> iceConfig = {
  <span class="hljs-attr">iceServers</span>: [
    {
      <span class="hljs-attr">urls</span>: <span class="hljs-string">"stun:stun.metered.ca:80"</span>
    },
    {
      <span class="hljs-attr">urls</span>: [
        <span class="hljs-string">"turn:global.relay.metered.ca:80"</span>,
        <span class="hljs-string">"turn:global.relay.metered.ca:80?transport=tcp"</span>,
        <span class="hljs-string">"turn:global.relay.metered.ca:443"</span>,
        <span class="hljs-string">"turns:global.relay.metered.ca:443?transport=tcp"</span>
      ],
      <span class="hljs-attr">username</span>: <span class="hljs-string">"your-credential-username"</span>,
      <span class="hljs-attr">credential</span>: <span class="hljs-string">"your-credential-password"</span>
    }
  ],
  <span class="hljs-attr">iceCandidatePoolSize</span>: <span class="hljs-number">2</span>
};

<span class="hljs-keyword">const</span> peerConnection = <span class="hljs-keyword">new</span> RTCPeerConnection(iceConfig);

<span class="hljs-comment">// Monitor ICE connection state changes</span>
peerConnection.oniceconnectionstatechange = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"ICE state:"</span>, peerConnection.iceConnectionState);

  <span class="hljs-keyword">switch</span> (peerConnection.iceConnectionState) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">"connected"</span>:
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Peer connected -- media flowing"</span>);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">"failed"</span>:
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">"ICE failed -- attempting ICE restart"</span>);
      peerConnection.restartIce();
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">"disconnected"</span>:
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">"Connection interrupted -- monitoring for recovery"</span>);
      <span class="hljs-keyword">break</span>;
  }
};

<span class="hljs-comment">// Monitor ICE candidate gathering</span>
peerConnection.onicecandidate = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (event.candidate) {
    <span class="hljs-comment">// Send candidate to remote peer via signaling channel</span>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"New ICE candidate:"</span>, event.candidate.type);
    <span class="hljs-comment">// event.candidate.type will be "host", "srflx", "relay", or "prflx"</span>
    signalingChannel.send({
      <span class="hljs-attr">type</span>: <span class="hljs-string">"ice-candidate"</span>,
      <span class="hljs-attr">candidate</span>: event.candidate
    });
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"ICE candidate gathering complete"</span>);
  }
};
</code></pre>
<p>Note the TURN configuration includes multiple transport options: UDP on port 80, TCP on port 80, UDP on port 443, and TLS on port 443 (<code>turns:</code>).</p>
<p>This layered approach maximizes connectivity. UDP is fastest, but some networks block non-standard UDP traffic. TCP on port 80 works through most firewalls.</p>
<p>TLS on port 443 (<code>turns:</code>) traverses even deep packet inspection (DPI) firewalls that inspect and block non-HTTPS traffic -- the TURN traffic looks like regular HTTPS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769907719888/27e2c8ff-f31c-4cb4-9bad-bb0ae2871009.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-cgnat-problem-why-nat-traversal-is-getting-harder">The CGNAT problem: why NAT traversal is getting harder</h2>
<p>If standard NAT wasn't challenging enough, carrier-grade NAT (CGNAT) adds another layer. And it's becoming more prevalent, not less.</p>
<h3 id="heading-what-cgnat-is">What CGNAT is</h3>
<p>CGNAT (also called Large Scale NAT or LSN) is a second layer of NAT deployed by internet service providers at the network level.</p>
<p>Your home router performs one level of NAT (private IP to router's public IP), and then the ISP's CGNAT gateway performs a second level (router's "public" IP to the ISP's actual public IP). Your device is now behind two NATs.</p>
<p>ISPs deploy CGNAT because they've run out of IPv4 addresses to assign to customers. Instead of giving each household a unique public IP, the ISP shares one public IP across dozens or hundreds of subscribers.</p>
<h3 id="heading-how-cgnat-affects-webrtc">How CGNAT affects WebRTC</h3>
<p>CGNAT creates several problems for NAT traversal:</p>
<p><strong>Double NAT breaks port mapping protocols.</strong> UPnP, NAT-PMP, and PCP only work on the first NAT hop -- your home router. The ISP's CGNAT upstream is unaffected.</p>
<p>You can open a port on your home router all day, and the ISP's NAT will still block inbound traffic.</p>
<p><strong>CGNAT behaves as symmetric NAT.</strong> ISP NAT gateways use endpoint-dependent mapping to maximize IP sharing efficiency. This means STUN-based hole punching fails.</p>
<p>Direct peer-to-peer connections are impossible without a relay.</p>
<p><strong>Shared IP addresses cause collateral damage.</strong> Cloudflare's <a target="_blank" href="https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/">2024-2025 research on CGNAT detection</a> revealed that shared IP addresses lead to "CGNAT bias" -- rate limiting and blocking that disproportionately impacts users behind shared IPs.</p>
<p>When one subscriber behind the CGNAT triggers a rate limit, every subscriber sharing that IP is affected.</p>
<h3 id="heading-cgnat-growth-trends">CGNAT growth trends</h3>
<p>CGNAT deployment is increasing, driven by continued IPv4 exhaustion:</p>
<ul>
<li><p><strong>Mobile networks</strong>: The majority of mobile carriers worldwide use CGNAT. If your users connect from phones on cellular data, they're almost certainly behind CGNAT.</p>
</li>
<li><p><strong>Emerging markets</strong>: ISPs in regions where IPv4 addresses were always scarce (South Asia, Africa, Latin America) rely heavily on CGNAT.</p>
</li>
<li><p><strong>Wireline ISPs</strong>: Even fixed-line providers are deploying CGNAT as IPv4 pools shrink.</p>
</li>
</ul>
<p>Academic research tracked CGNAT deployments growing from approximately 1,200 in 2014 to 3,400 in 2016, with mobile operators accounting for 28.85% of deployments. Growth has only continued since.</p>
<p>In practice, this means the percentage of WebRTC connections requiring TURN relay is trending upward, not downward. For applications with significant mobile or international user bases, a reliable <a target="_blank" href="https://www.metered.ca/stun-turn">TURN server</a> isn't a nice-to-have -- it's a requirement.</p>
<h2 id="heading-nat-traversal-beyond-webrtc">NAT traversal beyond WebRTC</h2>
<p>While this guide focuses on WebRTC, NAT traversal is a challenge across multiple domains. The fundamental problem -- establishing bidirectional communication through NATs -- is universal.</p>
<h3 id="heading-vpn-and-ipsec-nat-t">VPN and IPsec (NAT-T)</h3>
<p>IPsec VPN tunnels use ESP (Encapsulating Security Payload) packets, which NAT devices cannot translate because ESP doesn't use port numbers.</p>
<p>NAT-T (NAT Traversal, <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc3948">RFC 3948</a>) solves this by encapsulating ESP inside UDP on port 4500.</p>
<p>IKEv2 detects NAT presence during the initial handshake using <code>NAT_DETECTION_SOURCE_IP</code> and <code>NAT_DETECTION_DESTINATION_IP</code> payloads. If NAT is detected, both sides switch to UDP encapsulation automatically. Keep-alive packets (typically every 20 seconds) maintain the NAT mapping.</p>
<h3 id="heading-voip-and-sip">VoIP and SIP</h3>
<p>SIP (Session Initiation Protocol) embeds IP addresses in signaling headers and SDP bodies -- both the contact address and the media ports.</p>
<p>When SIP traverses a NAT, the internal addresses in the SIP headers don't match the external addresses on the packets. The result: the callee's phone rings, but audio flows nowhere because the media path uses the wrong addresses.</p>
<p>Solutions include STUN-based discovery (RFC 5626), SIP ALGs (Application Layer Gateways -- often more harmful than helpful), and ICE for SIP (<a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc5765">RFC 5765</a>).</p>
<h3 id="heading-gaming">Gaming</h3>
<p>Multiplayer games face the same NAT traversal challenge. Console platforms like Xbox and PlayStation use "NAT type" classifications (Open, Moderate, Strict) that roughly correspond to the classic cone/symmetric taxonomy.</p>
<p>"Strict NAT" players can only connect to "Open NAT" hosts. Games typically use relay servers (conceptually similar to TURN) as fallback, though many use proprietary relay protocols rather than standard TURN.</p>
<h3 id="heading-iot">IoT</h3>
<p>IoT devices behind home routers need to communicate with cloud services and sometimes directly with each other.</p>
<p>Most IoT platforms solve this with persistent outbound connections to cloud brokers (MQTT, CoAP), avoiding the NAT traversal problem entirely.</p>
<p>But peer-to-peer IoT scenarios -- direct camera-to-phone streaming, device-to-device mesh networks -- face the same NAT challenges as WebRTC and use similar techniques (STUN/TURN/ICE).</p>
<h2 id="heading-will-ipv6-eliminate-the-need-for-nat-traversal">Will IPv6 eliminate the need for NAT traversal?</h2>
<p>This is one of the most common questions in the NAT traversal space. The short answer: not anytime soon, and not entirely even then.</p>
<h3 id="heading-ipv6-eliminates-nat-but-not-firewalls">IPv6 eliminates NAT, but not firewalls</h3>
<p>IPv6 provides approximately 3.4 x 10^38 addresses -- enough for every device to have a globally unique, publicly routable address. In theory, this eliminates the need for NAT entirely. No NAT means no NAT traversal problem.</p>
<p>But firewalls still exist.</p>
<p>Even on pure IPv6 networks, stateful firewalls block unsolicited inbound connections by default. A stateful firewall tracking connections on the full 5-tuple (source IP, source port, destination IP, destination port, protocol) is functionally equivalent to a port-restricted cone NAT from a traversal perspective.</p>
<p>You still need hole punching or relay to establish peer-to-peer connections through firewalls.</p>
<h3 id="heading-current-ipv6-adoption">Current IPv6 adoption</h3>
<p>According to <a target="_blank" href="https://www.google.com/intl/en/ipv6/">Google's IPv6 statistics</a>, approximately 45-49% of Google traffic was IPv6 as of late 2025. The United States surpassed 50% in early 2025. France, Germany, and India lead with majority IPv6 traffic.</p>
<p>But adoption is uneven:</p>
<ul>
<li><p><strong>Corporate/enterprise networks</strong>: Many still run IPv4-only. Enterprises are notoriously slow to migrate.</p>
</li>
<li><p><strong>China</strong>: Less than 5% of Google traffic from China uses IPv6 (though government reports claim 865 million active IPv6 users).</p>
</li>
<li><p><strong>Weekday vs. weekend</strong>: IPv6 usage spikes on weekends (residential/mobile) and drops on weekdays (corporate), confirming that enterprise adoption lags behind.</p>
</li>
</ul>
<h3 id="heading-nat64-introduces-its-own-overhead">NAT64 introduces its own overhead</h3>
<p>For networks transitioning to IPv6-only, NAT64 translates between IPv6 and IPv4. This is itself a form of NAT, and it introduces performance penalties.</p>
<p>Research from Cornell University found that NAT64 paths are on average 23.13% longer with 17.47% higher round-trip times compared to native paths.</p>
<h3 id="heading-the-realistic-timeline">The realistic timeline</h3>
<p>IPv6 has been in deployment since the 1990s. Thirty years later, it still hasn't reached universal adoption.</p>
<p>Corporate networks, IoT devices running legacy stacks, and the massive installed base of IPv4-only equipment all ensure that NAT traversal will remain a necessary capability for years to come.</p>
<p>The pragmatic engineering approach: build for a world where NAT exists, and treat IPv6-only networks as a welcome simplification when you encounter them -- not as an excuse to skip NAT traversal.</p>
<h2 id="heading-the-future-of-nat-traversal-quic-webtransport-and-beyond">The future of NAT traversal: QUIC, WebTransport, and beyond</h2>
<p>The transport layer is evolving, and new protocols are changing how NAT traversal works -- though not eliminating the need for it.</p>
<h3 id="heading-quic">QUIC</h3>
<p>QUIC (<a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9000">RFC 9000</a>) runs over UDP, which is inherently more NAT-friendly than TCP.</p>
<p>QUIC's connection ID mechanism means that connections can survive NAT rebinding events (where the NAT assigns a new external port) without interruption. For WebRTC, this is significant: a user switching from Wi-Fi to cellular mid-call would historically break the TCP-based signaling connection and potentially disrupt media.</p>
<h3 id="heading-webtransport">WebTransport</h3>
<p>WebTransport is a new web API providing bidirectional, multiplexed transport using HTTP/3 (and therefore QUIC).</p>
<p>The IETF WebTransport specification (<a target="_blank" href="https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/">draft-ietf-webtrans-http3</a>) enables client-server communication with lower latency than WebSocket.</p>
<p>More relevant to NAT traversal: the W3C is developing a <a target="_blank" href="https://w3c.github.io/p2p-webtransport/">P2P WebTransport specification</a> that combines ICE-based NAT traversal with QUIC transport. This would bring QUIC's benefits (connection migration, multiplexing, reduced head-of-line blocking) to peer-to-peer communication -- while still using ICE, STUN, and TURN for connectivity establishment.</p>
<h3 id="heading-media-over-quic-moq">Media over QUIC (MoQ)</h3>
<p>Media over QUIC is an emerging IETF protocol for live media delivery.</p>
<p>While MoQ is primarily designed for server-based relay architectures (not peer-to-peer), it represents the broader industry trend toward QUIC-based real-time communication.</p>
<h3 id="heading-the-key-takeaway">The key takeaway</h3>
<p>Every emerging real-time protocol still needs ICE/STUN/TURN for peer-to-peer NAT traversal.</p>
<p>QUIC improves the transport layer, WebTransport modernizes the API surface, and MoQ rethinks media delivery -- but none of them solve the fundamental problem of discovering addresses and punching through NATs.</p>
<p>STUN and TURN infrastructure remains essential.</p>
<h2 id="heading-troubleshooting-nat-traversal-issues-in-webrtc">Troubleshooting NAT traversal issues in WebRTC</h2>
<p>When WebRTC connections fail, NAT traversal is the most common culprit. Here's a systematic approach to diagnosing and fixing these issues.</p>
<h3 id="heading-common-symptoms">Common symptoms</h3>
<ul>
<li><p><strong>"Works on my machine but not in production"</strong>: Connection succeeds on your office network (permissive NAT) but fails for users on corporate or mobile networks (restrictive NAT/CGNAT).</p>
</li>
<li><p><strong>Consistent ~20-30% failure rate</strong>: A significant minority of users can't connect. This is the classic "no TURN server" or "TURN misconfigured" signature.</p>
</li>
<li><p><strong>Connection hangs in</strong> <code>checking</code> state: ICE is attempting connectivity checks but no candidate pair succeeds.</p>
</li>
<li><p><strong>Connection reaches</strong> <code>failed</code>: All candidate pairs exhausted. No path works.</p>
</li>
<li><p><strong>Audio/video works initially then drops</strong>: NAT mapping timeout. The NAT discarded the mapping because keep-alive packets weren't sent frequently enough.</p>
</li>
</ul>
<h3 id="heading-step-by-step-diagnostic-process">Step-by-step diagnostic process</h3>
<p><strong>1. Check ICE candidate gathering</strong></p>
<p>Open <code>chrome://webrtc-internals</code> in Chrome (or the equivalent in your browser). Look at the ICE candidates gathered by each peer. You should see:</p>
<ul>
<li><p><strong>Host candidates</strong> -- If these are missing, the WebRTC API isn't accessing local addresses (rare).</p>
</li>
<li><p><strong>Server-reflexive (srflx) candidates</strong> -- If missing, your STUN server is unreachable or the NAT is blocking STUN traffic.</p>
</li>
<li><p><strong>Relay candidates</strong> -- If missing, your TURN server is unreachable, credentials are invalid, or TURN traffic is being blocked.</p>
</li>
</ul>
<p>If you only see host candidates, your STUN/TURN servers are not configured correctly or are unreachable from the user's network. Verify your configuration using a <a target="_blank" href="https://www.metered.ca/turn-server-testing">TURN server testing tool</a>.</p>
<p><strong>2. Analyze the selected candidate pair</strong></p>
<p>In <code>chrome://webrtc-internals</code>, find the active candidate pair. Check:</p>
<ul>
<li><p><strong>Candidate types</strong>: If the winning pair uses <code>relay</code> candidates, the connection went through TURN. This works but adds latency.</p>
</li>
<li><p><strong>Local and remote candidates</strong>: The candidate types tell you which NAT traversal technique succeeded.</p>
</li>
<li><p><strong>Round-trip time</strong>: High RTT on relay candidates may indicate the TURN server is geographically distant from one or both peers.</p>
</li>
</ul>
<p><strong>3. Check TURN server connectivity</strong></p>
<p>If relay candidates aren't being gathered, test TURN server connectivity:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Quick TURN connectivity test</span>
<span class="hljs-keyword">const</span> testConfig = {
  <span class="hljs-attr">iceServers</span>: [{
    <span class="hljs-attr">urls</span>: <span class="hljs-string">"turn:global.relay.metered.ca:443?transport=tcp"</span>,
    <span class="hljs-attr">username</span>: <span class="hljs-string">"test-username"</span>,
    <span class="hljs-attr">credential</span>: <span class="hljs-string">"test-credential"</span>
  }]
};

<span class="hljs-keyword">const</span> pc = <span class="hljs-keyword">new</span> RTCPeerConnection(testConfig);
pc.createDataChannel(<span class="hljs-string">"test"</span>);

pc.onicecandidate = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (event.candidate &amp;&amp; event.candidate.type === <span class="hljs-string">"relay"</span>) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"TURN relay candidate gathered -- TURN server is reachable"</span>);
    pc.close();
  }
};

pc.createOffer().then(<span class="hljs-function"><span class="hljs-params">offer</span> =&gt;</span> pc.setLocalDescription(offer));

<span class="hljs-comment">// If no relay candidate appears within 10 seconds, TURN is unreachable</span>
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (pc.signalingState !== <span class="hljs-string">"closed"</span>) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"No relay candidate -- TURN server unreachable or credentials invalid"</span>);
    pc.close();
  }
}, <span class="hljs-number">10000</span>);
</code></pre>
<p><strong>4. Implement ICE restart for recovery</strong></p>
<p>When a connection drops (NAT mapping timeout, network change), ICE restart can re-establish connectivity without creating a new peer connection:</p>
<pre><code class="lang-javascript">peerConnection.oniceconnectionstatechange = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (peerConnection.iceConnectionState === <span class="hljs-string">"failed"</span>) {
    <span class="hljs-comment">// Trigger ICE restart</span>
    peerConnection.restartIce();
    <span class="hljs-comment">// Create new offer with ICE restart flag</span>
    peerConnection.createOffer({ <span class="hljs-attr">iceRestart</span>: <span class="hljs-literal">true</span> })
      .then(<span class="hljs-function"><span class="hljs-params">offer</span> =&gt;</span> peerConnection.setLocalDescription(offer))
      .then(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// Send the new offer via signaling channel</span>
        signalingChannel.send({
          <span class="hljs-attr">type</span>: <span class="hljs-string">"offer"</span>,
          <span class="hljs-attr">sdp</span>: peerConnection.localDescription
        });
      });
  }
};
</code></pre>
<p><strong>5. Test from multiple network environments</strong></p>
<p>NAT traversal issues are network-dependent. Test from:</p>
<ul>
<li><p>Home Wi-Fi (consumer NAT -- usually permissive)</p>
</li>
<li><p>Mobile cellular data (likely CGNAT -- restrictive)</p>
</li>
<li><p>Corporate office network (firewall, potentially proxy-based)</p>
</li>
<li><p>VPN connections (adds another NAT layer)</p>
</li>
<li><p>Hotel/airport Wi-Fi (often highly restrictive)</p>
</li>
</ul>
<p>If connections succeed from home but fail from corporate or mobile networks, your TURN configuration is the likely issue.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769908289446/c2d7c0ff-8480-4dd1-bd13-9dc2575851ec.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-choosing-a-turn-server-for-reliable-nat-traversal">Choosing a TURN server for reliable NAT traversal</h2>
<p>NAT traversal theory is well-understood. The engineering challenge is operating reliable TURN infrastructure at scale.</p>
<p>For production WebRTC applications, here's what matters.</p>
<h3 id="heading-self-hosted-vs-managed">Self-hosted vs. managed</h3>
<p>You can deploy <a target="_blank" href="https://www.metered.ca/blog/coturn/">coturn</a> (the open-source TURN server) on your own infrastructure. It works.</p>
<p>But it comes with an operational burden: deploying across multiple regions for low latency, managing TLS certificates, handling auto-scaling for traffic spikes, rotating credentials, monitoring uptime, and patching security vulnerabilities.</p>
<p>Teams running coturn in production report spending 15-20 hours per month per engineer on TURN operations -- time that isn't going into building your actual product.</p>
<p>A managed TURN service eliminates that burden. You get an API call to provision credentials and global infrastructure that someone else operates.</p>
<h3 id="heading-what-to-look-for-in-a-managed-turn-service">What to look for in a managed TURN service</h3>
<ul>
<li><p><strong>Global coverage</strong>: Your TURN server should be close to your users. A TURN server in US-East doesn't help a user in Singapore -- it adds 250ms+ of latency to every packet.</p>
</li>
<li><p><strong>Multiple transport protocols</strong>: UDP, TCP, TLS, and DTLS. Different networks block different protocols. You need all four.</p>
</li>
<li><p><strong>Firewall-friendly ports</strong>: Port 80 and 443. Many corporate firewalls block non-standard ports.</p>
</li>
<li><p><strong>High availability</strong>: If your TURN server goes down, every relayed connection drops. 99.9% uptime means 8.7 hours of downtime per year. 99.999% means 5.3 minutes.</p>
</li>
<li><p><strong>Low latency</strong>: Every millisecond of TURN relay latency is added to your call quality. Sub-30ms from anywhere in the world is the benchmark.</p>
</li>
</ul>
<p><a target="_blank" href="https://www.metered.ca/stun-turn">Metered TURN Server</a> provides 31+ regions, 100+ PoPs, 99.999% uptime, sub-30ms latency, and support for UDP, TCP, TLS, and DTLS on ports 80 and 443. You can get started with a <a target="_blank" href="https://www.metered.ca/stun-turn">free trial</a> -- 500 MB of TURN usage, no credit card required. For a hands-on walkthrough, see the <a target="_blank" href="https://www.metered.ca/blog/guide-to-setting-up-your-webrtc-turn-server-with-metered/">setup guide</a>.</p>
<p>If you want to experiment with TURN without signing up for anything, the <a target="_blank" href="https://www.metered.ca/tools/openrelay/">Open Relay Project</a> provides a free community TURN server with 20 GB per month.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>NAT traversal is the invisible infrastructure challenge behind every WebRTC application. NATs break peer-to-peer connectivity by design, and the techniques to work around them -- STUN for address discovery, UDP hole punching for direct connections, and TURN for relay fallback -- are what make real-time communication actually work across the messy reality of the internet.</p>
<p>The landscape is getting harder, not easier. CGNAT deployments are growing as IPv4 exhaustion continues. Corporate firewalls remain restrictive.</p>
<p>IPv6 adoption, while progressing (45-49% of Google traffic), is decades away from universal and doesn't eliminate firewall traversal anyway. Emerging protocols like QUIC and WebTransport improve the transport layer but still rely on ICE/STUN/TURN for peer-to-peer connectivity establishment.</p>
<p>For production WebRTC, reliable TURN infrastructure is not optional. The 15-30% of connections that require relay aren't edge cases you can ignore -- they're real users on real networks who deserve to connect.</p>
<p>The engineering question is whether you want to operate that infrastructure yourself or let someone else handle it. If you'd rather spend your engineering hours on your actual product, <a target="_blank" href="https://www.metered.ca/stun-turn">Metered's managed TURN service</a> handles the relay infrastructure so you don't have to.</p>
<p>Start free -- 500 MB, no credit card.</p>
<h2 id="heading-frequently-asked-questions">Frequently asked questions</h2>
<h3 id="heading-what-is-nat-traversal">What is NAT traversal?</h3>
<p>NAT traversal is a set of techniques for establishing direct network connections between devices that are behind Network Address Translators (NATs). Because NATs hide devices behind shared public IP addresses, devices can't receive unsolicited inbound traffic.</p>
<p>NAT traversal solves this using address discovery (STUN), hole punching (coordinated simultaneous outbound packets), and relay servers (TURN) when direct connections fail.</p>
<h3 id="heading-what-is-the-difference-between-stun-and-turn">What is the difference between STUN and TURN?</h3>
<p>STUN discovers your public-facing IP address and port by asking a server on the public internet. It's lightweight, fast, and free to operate.</p>
<p>TURN relays all traffic through an intermediary server when direct connections are impossible (symmetric NATs, restrictive firewalls, CGNAT). TURN guarantees connectivity but adds latency and costs bandwidth.</p>
<p>In WebRTC, both are used together via the ICE framework -- STUN for direct connections when possible, TURN as fallback.</p>
<h3 id="heading-why-do-15-30-of-webrtc-connections-fail-without-turn">Why do 15-30% of WebRTC connections fail without TURN?</h3>
<p>About 15-30% of internet users sit behind symmetric NATs, CGNAT, or restrictive firewalls that prevent direct peer-to-peer connections.</p>
<p>STUN-based hole punching only works when NATs use endpoint-independent mapping. When the NAT assigns a different port per destination (endpoint-dependent mapping, or "symmetric NAT"), hole punching fails and TURN relay is the only path to connectivity.</p>
<h3 id="heading-does-ipv6-eliminate-the-need-for-nat-traversal">Does IPv6 eliminate the need for NAT traversal?</h3>
<p>IPv6 eliminates NAT but not firewalls. Stateful firewalls on IPv6 networks still block unsolicited inbound connections, which means hole punching and relay techniques remain necessary for peer-to-peer communication.</p>
<p>Additionally, IPv6 adoption is at roughly 45-49% globally (late 2025) and is unevenly distributed -- corporate networks significantly lag behind. NAT traversal will remain necessary for years.</p>
<h3 id="heading-how-do-i-troubleshoot-webrtc-connection-failures-caused-by-nat">How do I troubleshoot WebRTC connection failures caused by NAT?</h3>
<p>Start with <code>chrome://webrtc-internals</code> to inspect ICE candidate gathering and connection state.</p>
<p>Check whether server-reflexive (STUN) and relay (TURN) candidates are being gathered. If relay candidates are missing, verify TURN server reachability and credentials using a <a target="_blank" href="https://www.metered.ca/turn-server-testing">TURN server testing tool</a>.</p>
<p>Test from multiple network environments (home Wi-Fi, cellular data, corporate network) to identify which NAT types are causing failures. Implement ICE restart for recovery from transient failures.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769909289320/234fac9a-dcc8-4114-8d4b-8a7d9e11afe6.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item></channel></rss>