DEV Community

Cover image for STUN vs TURN vs ICE: the WebRTC networking explained
aprogrammer22
aprogrammer22

Posted on

STUN vs TURN vs ICE: the WebRTC networking explained

You copied a WebRTC tutorial, pasted in an iceServers config you didn't understand, and it worked on your laptop. Then you deployed, and the call connected for some users but hung forever for others.

The fix lives inside three acronyms everyone throws around and nobody defines: STUN, TURN, and ICE.

Here they are in one line each:

  • STUN discovers your public address — the IP and port the outside world actually sees you on.
  • TURN relays your media through a server when a direct peer-to-peer connection can't be made.
  • ICE is the process that gathers every possible path to your peer and picks one that works.

That's the whole story. The rest of this post unpacks each one using the config object you've already pasted a hundred times.

The config object this whole article is about

Every WebRTC app starts with something like this:

var myPeerConnection = new RTCPeerConnection({
  iceServers: [
      {
        urls: "stun:stun.relay.metered.ca:80",
      },
      {
        urls: "turn:global.relay.metered.ca:80",
        username: "846aaad2420e1ed06e8577d1",
        credential: "dsMcUouEAK+/bQkH",
      },
      {
        urls: "turn:global.relay.metered.ca:80?transport=tcp",
        username: "846aaad2420e1ed06e8577d1",
        credential: "dsMcUouEAK+/bQkH",
      },
      {
        urls: "turn:global.relay.metered.ca:443",
        username: "846aaad2420e1ed06e8577d1",
        credential: "dsMcUouEAK+/bQkH",
      },
      {
        urls: "turns:global.relay.metered.ca:443?transport=tcp",
        username: "846aaad2420e1ed06e8577d1",
        credential: "dsMcUouEAK+/bQkH",
      },
  ],
});
Enter fullscreen mode Exit fullscreen mode

iceServers is a list of helpers your browser can phone for help connecting to another peer.

There are exactly two kinds of helper in that list: a stun: server and a turn: server. Let's take them one line at a time.

The stun: line: STUN finds your public address

{ urls: "stun:stun.relay.metered.ca:80" }
Enter fullscreen mode Exit fullscreen mode

To explain this line, we need one piece of background: NAT.

Your computer's local IP (something like 192.168.1.x) is private. It only means something inside your home or office network. Your router hides everyone behind a single public IP and rewrites addresses on the way out — that's NAT (Network Address Translation).

Here's the problem: your peer can't reach you at 192.168.1.x. That address is meaningless on the open internet. You need to know the public address your router exposes you on.

That's STUN's entire job.

STUN (Session Traversal Utilities for NAT) is a tiny protocol defined in RFC 8489. Your browser sends one small request to the STUN server, and the server replies, "From where I'm sitting, I see you at this public IP and port." Think of it as holding up a mirror so you can see your own public face.

The address STUN hands back has a name: a server-reflexive candidate (often shortened to srflx). "Reflexive" because the server is reflecting your address back at you.

Once both peers know their public addresses, they can often connect directly — media flows peer-to-peer, and the STUN server steps out of the way. STUN is cheap, stateless, and used only during setup.

But sometimes a direct connection is impossible. That's where the second line comes in.

The turn: line: TURN relays your media

{
  urls: "turn:turn.example.com:3478",
  username: "myuser",
  credential: "mypass",
}
Enter fullscreen mode Exit fullscreen mode

Notice this entry has two things the STUN line didn't: a username and a credential. Hold that thought — it matters.

Some networks refuse to let two peers talk directly no matter how many public addresses they trade. Strict corporate firewalls and a type of NAT called symmetric NAT (more on that below) can block every direct path.

When direct fails, you need a middleman. That's TURN.

TURN (Traversal Using Relays around NAT) is defined in RFC 8656. A TURN server allocates a public address on itself and forwards all your media between the two peers. Both sides send their packets to the TURN server, and the server passes them along.

The address TURN allocates for you is called a relay candidate. It's a guaranteed-reachable address, because it lives on a public server.

So why the username and credential?

Relaying media is expensive. The TURN server burns real bandwidth pushing every video frame between two strangers.

Nobody wants to run a free relay for the entire internet, so TURN requires authentication. Those credentials prove you're allowed to use that relay.

In production, you don't hardcode a static password like the example above. You generate short-lived, time-limited credentials so a leaked username can't be abused forever. But the shape is the same: urls, username, credential.

Who runs the TURN server? You do, or a provider does. The open-source standard is coturn, which you self-host. Or you use a hosted TURN service so you don't run servers yourself. Either way, the config line looks identical.

ICE: the agent that ties it all together

Here's the part that confuses people: ICE isn't a server. There's no ice: line in the config. STUN and TURN are servers; ICE is the process running inside your browser that uses them.

ICE (Interactive Connectivity Establishment) is the framework defined in RFC 8445. When you create that RTCPeerConnection, the browser's ICE agent does three things:

  1. Gathers candidates. It collects every address you might be reachable on — your local interfaces, the public address STUN found, and the relay address from TURN.
  2. Trades them with the peer. Each candidate is sent to the other side through your signaling channel as it's discovered.
  3. Runs connectivity checks. ICE pairs up your candidates with the peer's and pings each pair to see which ones actually work, then picks the best one.

ICE always prefers the cheapest path. A direct connection beats a relayed one, so it tries host and STUN-discovered paths first and only falls back to the TURN relay when nothing else connects.

That's why STUN and TURN both live in the same iceServers array. You're not choosing between them — you're handing ICE both tools and letting it pick.

The three candidate types, side by side

Every address ICE gathers is called a candidate, and each has a type. These three are the ones you'll actually see:

Type Name Where it comes from Direct or relayed?
host Host candidate Your device's own network interface (e.g. 192.168.x.x) Direct (works on the same LAN)
srflx Server-reflexive Your public address, discovered via STUN Direct (works through most NATs)
relay Relay candidate A public address allocated by a TURN server Relayed (the fallback that almost always works)

There's a fourth type you may spot in logs, prflx (peer-reflexive), which ICE discovers during the connectivity checks themselves. As a beginner you can safely ignore it — the three above are the mental model that matters.

Why you need each one

Here's the categorical version of the pain, and the honest reason your call "works locally but dies in production."

On your machine, both tabs are on the same network. Your peers reach each other directly with host candidates. No STUN, no TURN — everything just works.

This is the trap: local success tells you nothing about the real internet.

In the wild, your users are behind NAT. Their host addresses are private and useless to each other. STUN's job is to find a public srflx address so a direct connection can still happen.

For a large share of real-world networks, STUN alone is enough.

Some networks block direct connections entirely. Two cases break STUN:

  • Symmetric NAT: some routers assign a different public port for every destination you talk to. The address STUN discovered (talking to the STUN server) won't match the address your peer needs (talking to your peer), so the direct path fails.
  • Strict firewalls: locked-down corporate or hotel networks may drop the peer-to-peer traffic outright, even when the addresses are correct.

When direct is impossible, TURN's relay candidate is the only thing that connects. That's why production apps configure TURN even though most connections never use it — it's the safety net for the connections that would otherwise silently fail.

The lesson: STUN is the optimist, TURN is the insurance policy, and ICE automatically takes the best path possible.

See it for yourself: log your candidate types

Want to watch ICE gather candidates? Paste this into your browser's DevTools console on any https:// page. It's a self-contained RTCPeerConnection that talks to no one — it just gathers and logs candidate types so you can see host, srflx, and relay in action.

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.metered.ca" },
    // Add a real turn: entry here to also see "relay" candidates:
    // { urls: "turn:turn.example.com:3478", username: "u", credential: "p" },
  ],
});

pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log(event.candidate.type, "", event.candidate.candidate);
  } else {
    console.log("Done gathering candidates.");
  }
};

// ICE only gathers candidates once there's something to negotiate,
// so create a data channel before making the offer.
pc.createDataChannel("probe");
pc.createOffer().then((offer) => pc.setLocalDescription(offer));
Enter fullscreen mode Exit fullscreen mode

A few things to notice when you run it:

  • You'll see host candidates immediately — those are your local interfaces. (Modern browsers show them as a random .local hostname instead of your raw 192.168.x.x address — that's an mDNS privacy feature, not a bug.)
  • With the Google STUN line, you'll get a srflx candidate showing your public address.
  • You'll only see relay candidates if you add a real, authenticated turn: entry. STUN can't produce them.
  • The final null candidate (logged as "Done gathering") is ICE telling you it has found everything.

Why the data channel? ICE won't gather candidates for a connection that has nothing to send. Adding a data channel (or a media track) before createOffer() gives ICE a reason to start. Drop that line and you'll see no candidates at all — a classic beginner gotcha.

A note on ports: 3478, 5349, and 443

You'll see these numbers in TURN/STUN URLs and wonder what they mean. Here's the short version, per the RFCs:

  • 3478 is the default port for STUN and TURN over plain UDP and TCP.
  • 5349 is the default port for STUN and TURN over TLS (the encrypted variant, written turns: with an s).
  • 443 isn't a STUN/TURN default at all — it's the standard HTTPS port. Providers often also offer TURN on 443 because locked-down firewalls almost always allow 443 outbound. Wrapping relayed media in something that looks like normal HTTPS traffic is how TURN sneaks through the strictest networks.

If your calls fail only on corporate or guest Wi-Fi, a TURN endpoint on 443 is frequently the thing that rescues them.

TURN Options

There are many options available today if you are looking for TURN servers

  1. Free TURN service: OpenRelayProject.org

  2. You can run your own turn servers using open source COTURN. Here is the guide: How to install COTURN

3.Paid TURN service with globally distributed TURN servers: Metered.ca TURN servers

Frequently Asked Questions

Is STUN enough on its own?

For many networks, yes. STUN finds your public address so peers can connect directly, and direct connections succeed across most home and mobile NATs. But STUN can't help when symmetric NAT or a strict firewall blocks the direct path — those connections need TURN. STUN alone is a gamble that works until it doesn't.

Do I really need TURN?

If you want your app to work for everyone, yes. Most connections won't use TURN, but a meaningful share of real-world networks (symmetric NAT, corporate firewalls) make a direct connection impossible. Without a TURN fallback, those users get a call that connects locally in testing and silently fails in production.

What's the difference between STUN and TURN in one sentence?

STUN tells you your public address so peers can connect directly; TURN relays your media through a server when a direct connection is impossible. STUN is a quick lookup used during setup; TURN is an active relay used for the entire call. ICE decides which one each connection actually needs.

What are ports 3478, 5349, and 443 about?

Port 3478 is the default for STUN and TURN over UDP/TCP, and 5349 is the default for the TLS-encrypted versions (stuns: / turns:). Port 443 is the HTTPS port; TURN is often offered there too because restrictive firewalls almost always allow 443, letting relayed media slip through disguised as normal web traffic.

Is ICE the same thing as STUN and TURN?

No. STUN and TURN are servers you connect to; ICE is the process inside your browser that uses them. ICE gathers candidate addresses (from your device, from STUN, and from TURN), trades them with the peer, runs connectivity checks, and picks a working path. There's no ice: line in your config — ICE is the agent over the top.

Wrapping up

Three acronyms, one mental model:

  • STUN finds your public address — the stun: line, producing srflx candidates.
  • TURN relays your media when direct fails — the turn: line with username and credential, producing relay candidates.
  • ICE gathers every candidate, checks which paths work, and picks one — no config line of its own, because it's the agent using the other two.

Next time you paste an iceServers block, you'll know exactly what each line does and why the second one needs a password. And when a call works on your laptop but breaks in production, you'll know the first thing to check: is TURN actually configured?


Sources: MDN — WebRTC connectivity, MDN — RTCIceCandidate.type, RFC 8489 (STUN), RFC 8656 (TURN), RFC 8445 (ICE). Last reviewed 2026-06-17.

Top comments (1)

Collapse
 
aprogrammer22 profile image
aprogrammer22

thanks for reading. I hope you liked the article