WebRTC apps that work perfectly on Wi-Fi often fail the moment a user switches to cellular data. Same code, same browser, same account, but the connection never establishes. This is one of the most common problems in production WebRTC deployments, and the cause is almost always the same: carrier-grade NAT (CGNAT) on mobile networks. In this post, we'll explain what CGNAT is, why it breaks WebRTC peer-to-peer connections, and how to fix it with a properly configured TURN server.

What Is Carrier-Grade NAT?

Mobile carriers ran out of IPv4 addresses years ago. The solution the industry settled on is carrier-grade NAT: a single public IP address shared among hundreds or thousands of subscribers, with the carrier's NAT equipment rewriting source ports on the way out and tracking state for return traffic.

This works fine for HTTP. Your phone makes an outbound TCP connection to a server, the carrier NAT remembers the mapping, and return packets find their way back. That's how the rest of the internet operates today.

WebRTC peer-to-peer is a different story. Two devices behind separate NATs need to discover each other's public addresses and exchange media packets directly. CGNAT makes both steps harder.

Why Mobile Networks Break WebRTC

There are two primary reasons cellular networks cause WebRTC connections to fail:

  • Symmetric NAT behavior: Many carrier NATs use a different external port for each destination. The address one peer discovers via STUN is useless to a different peer, because the NAT will reject incoming packets from any other source IP and port.
  • UDP packet drops: Some carriers prioritize TCP traffic and drop unsolicited UDP packets after short idle windows. Your media stream stutters, fragments, or stops entirely.

On top of this, some carriers (and corporate firewalls in coffee shops, hotels, and hospitals) block outbound UDP entirely as a security policy. That leaves a meaningful percentage of users for whom direct peer-to-peer simply cannot work.

Common Failure Symptoms

In your application, the CGNAT problem typically presents as one of three failure modes:

  • The call rings forever and never establishes.
  • One-way audio or video, where one peer can hear or see the other but not the reverse.
  • The call connects, then drops 30 seconds later when a NAT timeout expires.

If you check chrome://webrtc-internals on the failing peer, you'll see ICE candidates collected (host and server-reflexive) but the connectivity check failing. The browser tried to send packets to the other peer's announced address, and the other peer's NAT silently dropped them.

The Solution: TURN with Multiple Transports

The fix every production WebRTC application eventually adopts is to list a TURN server with multiple transport options in your iceServers config. ICE will try direct first, fall back to relayed, and pick whichever path actually carries packets.

Here's a complete example:

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.expressturn.com:3478' },
    {
      urls: [
        'turn:free.expressturn.com:3478?transport=udp',
        'turn:free.expressturn.com:3478?transport=tcp'
      ],
      username: 'YOUR_TURN_USERNAME',
      credential: 'YOUR_TURN_PASSWORD'
    }
  ],
  iceTransportPolicy: 'all'
});

The two TURN URLs each rescue a different category of broken network. By listing both, you let ICE pick whichever connects first.

Why You Need All Three Transports

  • UDP on port 3478: The cheapest and lowest-latency TURN path. Handles the simplest CGNAT case where direct UDP doesn't work peer-to-peer but does work to a fixed relay.
  • TCP on port 3478: Handles networks where UDP is dropped or rate-limited. Slightly higher latency but always available.
  • TLS on port 443: TURN over TLS on port 443 looks like ordinary HTTPS traffic to a firewall, so it gets through almost anything. Handles the most restrictive networks: hotel Wi-Fi blocking non-standard ports, hospital firewalls, locked-down corporate guest networks.

You don't have to choose. List all three. ICE picks the first one that establishes a working path.

Testing on a Real Mobile Network

Wi-Fi testing on the same LAN as your dev machine will never reproduce this bug. Two reliable ways to exercise the failure mode:

  1. Phone on cellular only. Turn off Wi-Fi on a real phone, browse to your app, and join a call. If your TURN config is wrong, this fails.
  2. Force-relay in the browser. Set iceTransportPolicy: 'relay' during testing to force every connection through TURN. If calls work in this mode, your TURN credentials are good. If they don't, fix that before chasing other bugs.
const pc = new RTCPeerConnection({
  iceServers: [/* your TURN config */],
  iceTransportPolicy: 'relay'  // force every connection through TURN
});

Open chrome://webrtc-internals while testing. The selected candidate pair should have type relay when force-relay is enabled. If it doesn't, your TURN config is broken and needs to be fixed before users hit the same issue in production.

Bandwidth Considerations

Adding TURN means your relay server carries the media for the users who need it. Some napkin math to size your TURN bandwidth:

  • A typical 720p video call on cellular (about 1.5 Mbps): roughly 675 MB per relayed hour, combined both directions.
  • An audio-only Opus call: roughly 30 MB per relayed hour.
  • If 25% of your calls go through TURN, multiply by your total call volume.

For most B2B SaaS apps with hundreds of daily calls, this comes out to tens of GB per month. The free tier on most managed TURN services covers it comfortably. Once you cross 1 TB of relayed traffic per month, flat-pricing TURN providers like ExpressTURN ($9/month for 5 TB) start beating metered providers by an order of magnitude.

Best Practices for Mobile WebRTC

  • Always include a TURN server: Don't ship a WebRTC app without one. Roughly 10 to 25% of real-world connections need a relay to complete.
  • List multiple transports: UDP, TCP, and TLS-443. ICE picks whichever works.
  • Test on real cellular: Wi-Fi testing will not reproduce CGNAT issues. Turn off Wi-Fi on a real phone and test the full call flow.
  • Use force-relay during development: iceTransportPolicy: 'relay' validates your TURN config before users do.
  • Monitor your TURN bandwidth: Unexpected spikes can indicate broken signaling, abuse, or unusual traffic patterns.

Wrapping Up

If your WebRTC app works on Wi-Fi and breaks on cellular, you almost certainly need TURN. Configure it with UDP, TCP, and TLS-443 transports, and the "doesn't work on my phone" support tickets disappear. The underlying problem isn't your code, it's how mobile carriers route traffic, and a properly configured TURN server gives WebRTC the relay path it needs to deliver media reliably across every network type.