e-speak

Quantifying Bandwidth and Processing Overhead in TURN Relays

By Nadia Ferrante

Wire Overhead and the Framing Penalty

Traversal Using Relays around NAT (TURN), specified in RFC 8656, solves symmetric NAT traversal by anchoring both signaling and media legs to an intermediate public IP address. The specification accomplishes address virtualization cleanly, but it introduces wire expansion that is frequently omitted during edge capacity planning.

"A TURN server is also a STUN server, so all TURN messages are STUN messages." — RFC 8656

Because all control interactions are Session Traversal Utilities for NAT (STUN) transactions, engineers often assume the data-plane encapsulation overhead matches the control plane. It does not. The wire overhead depends entirely on whether the client negotiates a channel binding or falls back to indication primitives.

ChannelData Versus Indication Framing

A standard conversational voice stream typically deploys the Opus codec operating with a 20 ms packetization interval (ptime), generating exactly 50 packets per second (pps). At an application target bitrate of 32 kbps, the raw audio frame is 80 bytes. Under standard Real-time Transport Protocol (RFC 3550) framing, the media layer adds a 12-byte RTP fixed header, yielding a 92-byte payload.

When routed directly between peers via UDP, the transport adds an 8-byte UDP header and a 20-byte IPv4 header (or 40 bytes under IPv6). The un-relayed packet on the wire is 120 bytes. At 50 pps, the layer-3 throughput is 48.0 kbps per direction.

When relayed over a TURN allocation using the Send and Data indications defined in RFC 8656 Section 6 and Section 7, overhead balloons. A Send Indication is a full STUN message:

The resulting STUN message is 128 bytes. Wrapped in UDP (8 bytes) and IPv4 (20 bytes), the wire footprint hits 156 bytes per packet. Throughput climbs to 62.4 kbps—a 30% expansion over direct peer-to-peer transport for the exact same 32 kbps encoded payload.

To reduce this bloat, clients issue a ChannelBind request. A successful binding assigns a 16-bit channel number (ranging from 0x4000 to 0x7FFF) to a specific peer transport address. Once established, media transfers use ChannelData framing:

The ChannelData header is precisely 4 bytes. Added to the 92-byte RTP packet, the payload is 96 bytes. Encapsulated in UDP and IPv4, the packet size becomes 124 bytes. At 50 pps, wire rate is 49.6 kbps. While this represents only a 3.3% increase over direct UDP framing, you must multiply this cost across both legs of the relay.

Multi-Leg Amplification Across Edge Networks

A TURN relay does not simply pass packets through an internal routing table; it terminates two distinct transport legs. For an allocation serving two endpoints communicating through the server:

1. Client to Relay: 124 bytes per packet (IPv4 + UDP + ChannelData + RTP). 2. Relay to Peer: 120 bytes per packet (IPv4 + UDP + RTP).

The relay node ingests 124 bytes and egresses 120 bytes for a single directional frame, handling 244 bytes of aggregate network traffic per 20 ms interval. In a bidirectional conversation, both participants send at 50 pps. The TURN node handles 200 packet operations per second for one single-channel call, processing 488 bytes every 20 ms.

Scaled to 10,000 concurrent two-party calls where both sides rely on the relay, the relay infrastructure must process 2,000,000 packets per second (2 Mpps) and switch 195.2 Mbps of sustained, symmetric throughput. If IPv6 is deployed on the client leg, add 20 bytes per packet. If clients fall back to TURN over TLS (typically on port 443 to bypass restrictive enterprise firewalls), TLS record framing (5 bytes) plus TCP framing (20–60 bytes) and MAC-layer framing destroy any expectation of wire efficiency.

Operating System State and Kernel Dispatch Costs

Bandwidth exhaustion is rarely what degrades a TURN relay at scale. Packet rate and kernel socket operations fail first.

Context Switches and epoll Boundary Overhead

A TURN server operating in userspace relies on epoll (or kqueue on BSD variants) to register readiness across thousands of file descriptors. In standard configurations, each allocation allocates at least one relayed socket bound to an ephemeral port allocated from the range configured by the operator.

At 2 Mpps, dispatching individual packets via recvfrom() and sendto() invokes 4,000,000 system calls per second. The CPU spends the majority of its execution budget servicing kernel-to-userspace transitions, copying packet buffers, and updating virtual memory page tables. Using recvmmsg() and sendmmsg() mitigates system call frequency by batching up to 32 or 64 messages per call, but it introduces a trade-off: batching holds packets in kernel buffers until the batch size or timeout is reached, which artificially injects jitter into time-sensitive audio streams.

` +--------------------------------------------------------------+

Userspace TURN Daemon
+---------------------+ +----------------------+
5-Tuple Hash TableChannel/Perm Table

+--------------|-------------------------------|---------------+ | recvmmsg() | sendmmsg() +--------------|-------------------------------|---------------+

+----------+----------+ +----------+-----------+
Linux Kernel
v v
[ UDP Socket Buffer ] [ UDP Socket Buffer ]
^
Core Network v

+--------------------------------------------------------------+ `

Furthermore, socket buffer memory scales linearly with concurrency. If SO_RCVBUF and SO_SNDBUF are set to a conservative 256 KB to absorb transient network bursts without dropping voice frames, a single allocation supporting bidirectional relaying consumes 1 MB of non-swappable kernel slab memory across its pair of sockets. For 10,000 active allocations, the operating system pins 10 GB of RAM purely to maintain socket buffers, independent of the userspace daemon's internal state.

Allocation Table Memory and Ephemeral State

Every active TURN allocation maintains state dictated by the state machines in RFC 8445 (Interactive Connectivity Establishment) and RFC 8656. The relay must track:

By default, an RFC 8656 allocation has a lifetime of 600 seconds (10 minutes). Permissions expire after 300 seconds (5 minutes) unless refreshed by continuous traffic or explicit CreatePermission transactions. Channel bindings expire after 600 seconds unless kept alive by a ChannelBind request.

This state introduces locking and cache line invalidation inside the userspace daemon. For every ingress packet, the server executes a hash table lookup to match the client's source 5-tuple to an active allocation, checks the permission or channel-binding table, validates that the binding has not expired, rewrites the IP/UDP or ChannelData header, and schedules the packet for transmission on the secondary socket. If this hash table is protected by a coarse-grained mutex, worker threads scale poorly beyond 4 to 8 physical CPU cores. High-throughput designs require sharding the 5-tuple space across separate threads via SO_REUSEPORT, pinning each thread to a dedicated CPU core to eliminate inter-core synchronization.

Path Dynamics and Degradation Under Stress

When network conditions deteriorate, the operational characteristics of a TURN relay shift unpredictably, frequently exacerbating the underlying network issues.

Under elevated packet loss (approaching 8% to 15%) or transient packet delay variation (jitter exceeding 60 ms), client-side media engines adapt their transmission strategies. The Opus encoder enables in-band Forward Error Correction (FEC), embedding a redundant copy of the previous frame inside the current frame at a lower bitrate. If packet loss persists, transport-wide congestion control algorithms (such as those providing feedback via RFC 8888) signal the encoder to slash primary bitrate while increasing FEC allocation.

Crucially, the packet cadence does not drop; it remains locked at 50 pps. However, downstream jitter buffers—such as WebRTC's NetEQ module—expand their playout delay targets from an initial 20–40 ms up to 200 ms or more to accommodate arrival delta variance.

` Direct: [ Client A ] ------------------------------> [ Client B ] Targeted Jitter: ~15ms

Relayed: [ Client A ] ------> [ TURN Server ] ------> [ Client B ] Jitter: ~15ms Jitter: ~35ms Combined Jitter: ~50ms (Buffer expands) `

The relay itself acts as a multiplier of network impairments: 1. Loss Correlation: On a direct peer-to-peer path, a dropped packet is dropped once. On a relayed path, packet loss on the Client-to-Relay segment invalidates the processing capacity already spent by the relay, while loss on the Relay-to-Peer segment occurs after the ingress compute budget has already been exhausted. 2. Bufferbloat Interaction: When a client falls back to TURN over TCP due to UDP egress blocking, packet loss triggers TCP's sliding-window congestion control and head-of-line blocking. A single dropped segment stalls all subsequent RTP packets in the kernel buffer until the missing segment is retransmitted. The receiving jitter buffer rapidly starves, causing continuous audio dropouts. TURN over TCP is functionally incompatible with high-quality real-time voice; it acts as a transport of last resort. 3. Timer Expiration During Silent Periods: Real-time audio clients often utilize Discontinuous Transmission (DTX), halting packet output during periods of silence. If a participant remains muted for longer than the 300-second permission lifetime, the relay's permission state drops silently unless the client actively transmits STUN keep-alive indications. Once dropped, subsequent inbound media from the peer is rejected by the server with ICMP port unreachable or dropped entirely, truncating the first words spoken when the user unmutes. I would check this behavior across different client WebRTC library versions, as implementation compliance varies widely regarding background STUN refresh intervals during prolonged silence.

The Practical Rule

Calculate your relay hardware budget strictly on packets per second, not megabits per second: take your maximum expected concurrent relayed voice calls, multiply by 200 packets per second to cover bidirectional data across both legs, add 10% for ICE connectivity checks and STUN refreshes, and size your CPU allocation to ensure no single core ever exceeds 250,000 packets per second using batched system calls.

Client Ingress (pps) Peer Egress (pps)