If you've shipped a WebRTC project, you've probably hit the moment when calls work flawlessly between you and a colleague on the same network, then fail mysteriously the moment a real user tries to connect from a coffee shop or their parents' house. Almost every time, the cause is the same: the peer-to-peer connection couldn't pierce the NAT or firewall sitting between the two endpoints.
Two protocols solve that problem: STUN and TURN. They sit at the heart of every reliable WebRTC deployment, and most teams treat them as a single thing. They aren't. Knowing which one is doing the work in each connection matters when you're debugging, when you're sizing your infrastructure, and especially when you're paying for bandwidth.
The Problem They Both Solve
WebRTC's promise is direct browser-to-browser media. No relay, no SaaS hop,
just two peers exchanging audio, video, or data over UDP. The catch: most
endpoints today are behind a NAT. The browser's view of its own address
(192.168.1.42, 10.0.0.5, etc.) is not what the rest
of the internet sees. Without help, two NAT'd peers can't even tell each
other where to send packets.
STUN and TURN are the two strategies that let them figure it out.
STUN: Ask "What Do I Look Like From Out There?"
STUN, Session Traversal Utilities for NAT, is a tiny query/response protocol. The client sends a STUN request to a public STUN server. The server replies with the source IP and port it observed. That tells the client its own public-facing address, which it can then share with the other peer through the signaling channel.
If both peers learn their public addresses this way, and at least one of the NATs is permissive enough to accept incoming UDP from the other side, a direct media path opens. No relay involved.
This works for the majority of home internet connections. It also covers most office networks where outbound UDP isn't blocked. The cost to operate is trivial, STUN traffic is essentially a single round-trip per peer, no media flowing through the STUN server itself.
You'll see STUN in your RTCPeerConnection config as something
like:
{ urls: 'stun:stun.expressturn.com:3478' }
When STUN Isn't Enough
STUN fails predictably in a few specific situations:
- Symmetric NAT on either side. The NAT maps each destination to a different external port, so the address STUN reported is useless to a different peer.
- Carrier-grade NAT (CGNAT), common on mobile networks and increasingly on residential ISPs that ran out of IPv4 space.
- Strict corporate firewalls that block outbound UDP entirely, or only allow specific ports.
- Hotel and conference Wi-Fi, these block UDP for "security" reasons surprisingly often.
In all of these cases, the two peers cannot reach each other directly, no matter how cleverly they share addresses. They need a man in the middle to relay the packets.
TURN: When You Need a Relay
TURN, Traversal Using Relays around NAT, is exactly that man in the middle. The client allocates a port on the TURN server, advertises that allocation as its candidate address, and then any media destined for it gets forwarded through the relay.
It costs more than STUN: every byte of media flows through the TURN server in both directions, so a one-hour 1080p video call relayed through TURN is about 1.35 GB of bandwidth. The TURN server is doing real work and consuming real network capacity.
In return, you get connectivity that essentially never fails. TURN over TCP and TURN over TLS on port 443 will traverse virtually every firewall on earth, they look like ordinary HTTPS traffic. If a user can browse the web at all, they can use your WebRTC application.
How They Work Together
This is where most tutorials get confusing. STUN and TURN aren't either/or. ICE, the algorithm WebRTC uses to negotiate paths, collects candidates from both in parallel and then picks the best one that works.
The order of preference is roughly:
- Host candidate, direct LAN connection, when both peers are on the same network.
- Server-reflexive candidate, the STUN-discovered public address, when one or both NATs are permissive.
- Relayed candidate, the TURN allocation, when nothing else worked.
So in practice, you list both STUN and TURN servers in your
iceServers array. Most of your traffic will use STUN paths and
cost you almost nothing. The minority of users who genuinely need TURN will
be quietly relayed without any change to your client code.
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.expressturn.com:3478' },
{
urls: [
'turn:relay1.expressturn.com:3478?transport=udp',
'turn:relay1.expressturn.com:3478?transport=tcp',
'turns:relay1.expressturn.com:443?transport=tcp'
],
username: 'YOUR_TURN_USERNAME',
credential: 'YOUR_TURN_PASSWORD'
}
]
});
Why You Want All Three Transports
Notice the three TURN URLs in that snippet. They're not redundant, each one rescues a different category of broken network:
turn:...:3478?transport=udp, the cheapest and lowest-latency TURN path. Most relayed users will land here.turn:...:3478?transport=tcp, for networks that block UDP entirely. Slightly higher latency but always available.turns:...:443?transport=tcp, TURN over TLS on port 443. Looks like regular HTTPS to a firewall, gets through almost anything.
Listing all three lets ICE pick whichever connects first. The browser does the work, your code stays simple.
Authentication Patterns
Both long-term credentials and time-limited ones are part of the standard. Long-term credentials are a static username/password pair you bake into your client config. They're fine for early development and small deployments.
For production at scale, the cleaner pattern is shared-secret authentication: your server generates a fresh credential for each session using HMAC-SHA1 over an expiry timestamp. The credential is valid for a short window. A leaked client bundle therefore can't drain your TURN account forever, the leaked credential expires within an hour. The TURN spec describes this in RFC 5766 Section 4. Most managed providers, including ExpressTURN, support both.
Verifying TURN Actually Works
The classic embarrassment is to deploy with TURN configured, never exercise the TURN path because your dev machine connects directly, and discover in production that the TURN credentials were never quite right. Avoid that by force-relay testing during development:
const pc = new RTCPeerConnection({
iceServers: [/* your config */],
iceTransportPolicy: 'relay' // every packet goes through TURN
});
Open chrome://webrtc-internals while a call is connected.
The selected candidate pair should be of type relay. If it
isn't, your TURN config is broken, fix it before you ship.
Bandwidth and Cost
Because TURN actually carries the media, it costs real money. The napkin math:
- A one-hour Opus voice call relayed: ~30 MB combined both directions.
- A one-hour 720p video call relayed: ~675 MB combined.
- A one-hour 1080p call relayed: ~1.35 GB combined.
Multiply by the share of your users who actually need TURN, usually 10-25%, to estimate your monthly relay bandwidth. Self-hosted coturn on a cheap VPS works for small workloads. At scale, a managed provider with flat pricing (e.g. ExpressTURN at $9/month for 5 TB) is dramatically cheaper than metered providers like Twilio Network Traversal.
The Short Version
STUN finds your public address so peers can talk directly. TURN relays your media when peers can't talk directly. Use both, list multiple TURN transports, force-relay in dev, and pick a TURN provider whose pricing matches your traffic profile. That's most of WebRTC NAT traversal in practice.
Want a free TURN endpoint to test with? Grab credentials at ExpressTURN, 1 TB/month free, no credit card.