When a WebRTC infrastructure bill starts climbing, the culprit is almost never CPU and almost always a TURN relay moving bytes it perhaps did not need to move. Signaling is tiny. STUN is a handful of packets. But a TURN server sits in the media path and forwards every frame of every relayed call, and bandwidth is what cloud providers charge real money for. The good news is that relay traffic is one of the most controllable line items in real-time communication — if you know how to measure it and where the waste hides.
This piece walks through the cost model, how to measure your own relay ratio, how to stop relaying calls that should have gone direct, and how the self-hosting and managed economics actually compare in 2026.
Why TURN costs money: every byte twice
A TURN server is a relay. A packet from peer A arrives at the server (ingress) and leaves toward peer B (egress). The same is true in reverse. So a relayed call pushes its entire media volume through the server, and on metered clouds you typically pay for the egress half of that — sometimes both halves.
Put numbers on it. A modest video call runs around 1.5 Mbps in each direction. Convert to bytes per hour for one direction:
1.5 Mbps = 1.5 / 8 = 0.1875 MB/s
0.1875 MB/s × 3600 s = 675 MB/h ≈ 0.675 GB/h per direction
A call has two directions (A→B and B→A):
0.675 GB/h × 2 = 1.35 GB through the relay per hour of call
So one hour of one relayed 1.5 Mbps video call is roughly 1.35 GB of relay traffic. Ten thousand such call-hours in a month is 13.5 TB. At hyperscaler egress rates that is a four-figure bill for a single feature — which is why the rest of this article is about not relaying calls you do not have to.
First, measure your relay ratio
Before optimizing anything, find out what fraction of your connections actually use TURN. For most consumer-web audiences only about 10–20% of connections need a relay; the rest succeed host-to-host or via server-reflexive (STUN) candidates. That number is not universal — enterprise networks behind strict firewalls, and mobile-heavy audiences stuck behind carrier-grade NAT, push it well higher — so measure your own traffic rather than trusting a blog's average (including this one).
The most accurate source is the client. Read the selected candidate pair from getStats() and
report the relay flag back to your analytics:
const stats = await pc.getStats();
let localType, remoteType;
stats.forEach((report) => {
if (report.type === 'candidate-pair' && report.nominated && report.state === 'succeeded') {
const local = stats.get(report.localCandidateId);
const remote = stats.get(report.remoteCandidateId);
localType = local && local.candidateType; // host | srflx | relay
remoteType = remote && remote.candidateType;
}
});
const usedRelay = localType === 'relay' || remoteType === 'relay';
analytics.track('ice_path', { localType, remoteType, usedRelay });
On the server side, coturn can tell you the same story in aggregate. Its logs record allocations and session byte counts, and the built-in Prometheus exporter exposes per-session and total traffic metrics you can graph. Cross-checking client candidate types against server byte totals also catches the worst case: relays carrying traffic for sessions that should have connected directly.
Make sure TURN isn't used unnecessarily
ICE already prefers cheaper paths. Each candidate carries a priority, and host and server-reflexive candidates are ranked above relay, so when a direct path works the connection checks pick it automatically. Your job is to not sabotage that.
The most expensive mistake is shipping iceTransportPolicy: 'relay' in production. That flag
forces every connection through TURN, discarding all the direct paths ICE would have found. It is
a useful diagnostic for confirming your relay works, but leaving it on in production means paying relay
bandwidth for 100% of calls instead of 10–20%.
// FINE for a one-off relay test:
const pc = new RTCPeerConnection({ iceServers, iceTransportPolicy: 'relay' });
// WRONG for production — forces 100% of calls through TURN:
// iceTransportPolicy: 'relay'
// CORRECT for production — let ICE choose; relay is the fallback:
const pc = new RTCPeerConnection({ iceServers }); // default 'all'
The quieter, costlier failure is broken STUN. If your STUN server is misconfigured or unreachable, clients
never gather server-reflexive candidates, so peers that could have connected directly silently fall
back to TURN — and your relay bill creeps up with no error anywhere. A working STUN setup is the single
biggest lever for keeping relay traffic low, which is why verifying it is worth doing deliberately. Run your
STUN and TURN URLs through the ICE Server Tester and confirm you actually get
srflx candidates back; if you only ever see relay, STUN is the thing to fix first.
For the underlying mechanics of when relaying is genuinely required, see our
guide to TURN servers.
Self-hosting economics: it's bandwidth, not CPU
The defining fact of self-hosted TURN is that coturn is cheap to run and expensive to feed. A single modest VPS core can relay tens of thousands of concurrent allocations — packet forwarding is light work. What you pay for is the bandwidth flowing through it, and there the provider you choose can change the bill by an order of magnitude.
Hyperscalers meter egress aggressively. As of 2026, AWS and GCP list outbound transfer on the order of $0.08–0.12 per GB (tiered, region-dependent, and subject to change). Bandwidth-generous VPS providers price the opposite way: Hetzner, OVH, and similar hosts bundle large monthly transfer allowances — often tens of terabytes — into a flat server fee, with cheap or capped overage. For a workload that is almost pure egress, the difference is stark.
Scenario: 10 TB / month of TURN relay egress
Hyperscaler @ ~$0.10/GB:
10,000 GB × $0.10 = ~$1,000 / month (plus the instance)
Bandwidth-generous VPS:
A mid-tier server with a ~20 TB transfer allowance
often runs ~$5–$50 / month total, traffic included.
Those VPS numbers are illustrative and vary by plan and region, but the shape is real: for relay you are buying bandwidth, so host the relay where bandwidth is bundled, not where every gigabyte is metered. Put a tiny instance on a transfer-generous provider and you have turned a four-figure egress line into a flat monthly fee.
Managed TURN: the build-vs-buy math
If you would rather not operate relays, several providers sell TURN by the gigabyte or by subscription. Treat every figure below as approximate and 2026-dated — these prices move:
| Provider | Model (approx, 2026) | Notes |
|---|---|---|
| Twilio NTS | ~$0.40/GB historically | Convenient, broad coverage; the priciest per GB |
| Cloudflare TURN | On the order of cents/GB | Markedly cheaper per GB; large edge footprint |
| Xirsys / Metered | Subscription tiers + overage | Predictable monthly plans; good for steady volume |
Work an example at 10 TB/month of relay. At a Twilio-style ~$0.40/GB that is roughly $4,000; at a cents-per-GB service it can fall to the low hundreds; self-hosted on a transfer-generous VPS it approaches a flat double-digit fee plus your own operational time. The crossover is about volume and appetite for ops: managed wins at low or spiky volume where you do not want to babysit servers; self-hosting wins decisively once you are pushing many terabytes every month.
Regional placement: latency is a cost too
A relay adds a hop, so where it sits directly affects call quality. Routing a call between two users in Frankfurt through a relay in Virginia sends every packet across the Atlantic twice, adding well over a hundred milliseconds of round trip for no benefit. Relay near the users.
A sane starting point is one relay per major continent — for example North America, Europe, and Asia — with clients steered to the closest one. You can do this with GeoDNS resolving a single TURN hostname to the nearest region, or by handing each client a region-specific list of TURN URLs from your signaling server based on their source IP. Either way the principle holds: the nearest working relay is the cheapest in both latency and, often, inter-region transfer fees.
coturn tuning for cost and safety
An open or unbounded relay is a bandwidth liability — abusers will happily use your server to proxy their own traffic. A few coturn directives cap exposure and stop leeching:
# /etc/turnserver.conf
# --- Quotas: cap how much any user or the whole server can move ---
total-quota=1200 # max simultaneous allocations server-wide
user-quota=12 # max allocations per user
max-bps=2000000 # per-session cap, ~2 Mbps
bps-capacity=0 # 0 = unlimited aggregate; set to cap total
# --- Time-limited REST credentials (turn_secret + TTL) ---
use-auth-secret
static-auth-secret=REPLACE_WITH_LONG_RANDOM_SECRET
realm=turn.example.com
# --- Stop the relay from reaching internal/private networks ---
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
no-multicast-peers
# --- Metrics ---
prometheus
The single most important anti-abuse measure is time-limited credentials. With
use-auth-secret and a shared static-auth-secret, your application mints short-lived
usernames of the form timestamp:userid with an HMAC password, so a credential someone scrapes
from your client expires in minutes instead of granting permanent free relay. Generate them like this:
# username = unix_expiry_timestamp ":" user_id
# password = base64( HMAC-SHA1( static_auth_secret, username ) )
EXPIRY=$(( $(date +%s) + 3600 )) # valid 1 hour
USER="$EXPIRY:alice"
PASS=$(printf '%s' "$USER" \
| openssl dgst -binary -sha1 -hmac "REPLACE_WITH_LONG_RANDOM_SECRET" \
| openssl base64)
echo "username=$USER"
echo "credential=$PASS"
The denied-peer-ip ranges and no-multicast-peers prevent the classic open-relay
attack where someone uses your TURN server to reach machines on your internal network. The quotas keep one
abusive user from saturating the link, and prometheus gives you the per-session byte counts you
need to actually see your relay ratio. Setup details are in our
coturn installation guide.
Keep TCP/TLS as a last resort, not the default
TURN over UDP is the cheap, fast path. TURN over TCP, and especially TURN over TLS on port 443, cost more
on both axes: TLS adds CPU for encryption, TCP's retransmission and head-of-line blocking add latency that
hurts real-time media, and connection setup is heavier. You still need a 443/TLS listener — it is what gets
calls through restrictive corporate firewalls that block UDP entirely — but it should be the fallback ICE
reaches only after UDP and plain TCP fail, never the primary. Order your iceServers so the
UDP turn: URL comes first and the turns: on 443 comes last, and the engine will
prefer the cheaper transport whenever the network allows it.
Summary: the cost-reduction checklist
| Action | Why it cuts cost |
|---|---|
| Measure relay ratio via getStats / coturn metrics | You can't optimize what you haven't quantified; reveals waste |
| Verify STUN actually returns srflx candidates | Broken STUN silently dumps direct-capable calls onto TURN |
Never ship iceTransportPolicy: 'relay' |
Forces 100% of calls through paid relay instead of ~10–20% |
| Self-host on a transfer-generous VPS | Relay is bandwidth, not CPU; avoids metered egress rates |
| Place a relay per continent | Cuts cross-region latency and inter-region transfer fees |
| Time-limited REST credentials + quotas | Stops credential leeching and open-relay bandwidth theft |
denied-peer-ip + no-multicast-peers |
Blocks relay abuse toward internal networks |
| UDP first, TLS/443 last | Avoids extra CPU and latency of TCP/TLS relay when avoidable |
Do the first three and most teams cut their relay traffic substantially overnight, because the common case is misconfiguration quietly routing direct-capable calls through TURN — not genuine demand for relaying. The rest is about hosting the unavoidable relay traffic as cheaply and safely as possible. If you are still choosing a media topology, our SFU vs MCU vs mesh comparison covers how architecture choices upstream of TURN also shape your bandwidth bill.
Check Your Own Servers
Misconfigured STUN is the quietest cause of a runaway TURN bill — it pushes direct-capable calls onto paid
relay with no error. Run your STUN and TURN URLs through the tester and confirm you're getting
srflx candidates, not just relay.