−25%

on annual Windows plans, until 31 Oct. See plans

EQVPS
Get started

VPS for a TURN server (coturn) for WebRTC

Calls that fail behind strict NATs need a TURN relay. Run coturn on a server with its own IPv4: ports, time-limited credentials, a tested config and sizing.

Your WebRTC app works on your Wi-Fi and fails for the one customer calling from a hotel. Peer-to-peer media needs a path between the two sides; when both are behind strict NAT, or a firewall drops UDP, there's no path and the call just doesn't connect. Nobody sees an error — just an endless „connecting…“.

A TURN server is the fallback for those calls: both sides send their media to it, and it relays. If you're building video or voice into an app, running Matrix or Jitsi for a team, or doing voice chat for a game, you want one. coturn is the standard open-source implementation, and it's light.

Which plan

TURN is cheap on CPU and memory. What it uses is bandwidth: each relayed stream goes through the server twice, in and out. Traffic on our plans is unmetered at 1 Gbit/s, so the question is how many relayed calls run at once, not how much you'll be billed.

Relayed calls at the same timePlan
A handful — a team, a small appNano-IP — 2 vCPU, 1 GB, 15 GB ($8/mo)
DozensMicro-IP — 2 vCPU, 2 GB, 25 GB ($10/mo)
More, or TURN next to your app serverSmall-IP — 4 vCPU, 4 GB, 35 GB ($16/mo)

It has to be a plan with its own IPv4 — TURN is all about inbound connections on many ports, and NAT plans only accept SSH from outside. On our dedicated-IP plans the public address sits right on the server's interface, so coturn finds it without an external-ip line. Put the server close to your users: Germany or Finland, availability is shown at checkout. Crypto payment, no KYC, if that matters to you.

Ports to open: 3478 UDP and TCP, 49152–65535 UDP for the relayed media, and 5349 TCP if you add TLS.

Install and configure

As root on Ubuntu 24.04 or Debian 12:

apt install -y coturn
ip -4 addr    # your public IP should be listed on the interface

Write the config with a fresh shared secret:

SECRET=$(openssl rand -hex 32)
cat > /etc/turnserver.conf <<EOF
listening-port=3478
fingerprint
use-auth-secret
static-auth-secret=$SECRET
realm=turn.example.com
min-port=49152
max-port=65535
no-cli
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
EOF
echo "$SECRET"    # your backend needs this value
systemctl restart coturn
ufw allow 22/tcp && ufw allow 3478 && ufw allow 49152:65535/udp && ufw enable

Don't skip the denied-peer-ip lines. Without them, anyone with valid credentials can use your relay to reach private addresses — including services on the server's own internal side. We checked: with these lines a relay to a private address is refused with „403 Forbidden IP“.

Credentials that expire

With use-auth-secret, coturn accepts a username that contains an expiry time and a password that's an HMAC of it. Your backend generates them; the secret never leaves the server side. In Node.js:

const crypto = require("crypto");

// Temporary TURN username/password (the TURN REST API scheme coturn understands via use-auth-secret).
function turnCredentials(secret, user, ttlSeconds = 3600) {
  const username = `${Math.floor(Date.now() / 1000) + ttlSeconds}:${user}`;
  const credential = crypto.createHmac("sha1", secret).update(username).digest("base64");
  return { username, credential };
}

console.log(JSON.stringify(turnCredentials(process.env.TURN_SECRET, "alice")));

In the browser, ask your backend for a fresh pair and hand it to RTCPeerConnection:

const { username, credential } = await fetch("/api/turn-credentials").then((r) => r.json());

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:turn.example.com:3478" },
    {
      urls: ["turn:turn.example.com:3478?transport=udp", "turn:turn.example.com:3478?transport=tcp"],
      username,
      credential,
    },
  ],
});

Test it

From the server, using the same scheme with openssl (it produces exactly the same password as the Node.js code — we compared):

U="$(( $(date +%s) + 3600 )):test"
P=$(echo -n "$U" | openssl dgst -binary -sha1 -hmac "$SECRET" | base64)
turnutils_uclient -y -n 10 -u "$U" -w "$P" YOUR_SERVER_IP

At the end you want „Total lost packets 0“. With a wrong password the client stops at „Cannot complete Allocation“ — that's the server doing its job.

We also checked from a real Chromium: with iceTransportPolicy: "relay" and fresh credentials the browser got relay candidates from the server; with a bad password it got none. If you want to test your own setup the same way, set that option for a moment — it forces every call through TURN.

TLS, if you need it

Some networks block UDP and are picky about plain TCP; TURN over TLS gets through more of them. Get a certificate for turn.example.com (certbot is fine). Its files are readable by root only, while coturn runs as the turnserver user, so give coturn its own copy:

mkdir -p /etc/coturn
cp /etc/letsencrypt/live/turn.example.com/fullchain.pem /etc/letsencrypt/live/turn.example.com/privkey.pem /etc/coturn/
chown turnserver: /etc/coturn/*.pem && chmod 640 /etc/coturn/privkey.pem
cat >> /etc/turnserver.conf <<'EOF'
tls-listening-port=5349
cert=/etc/coturn/fullchain.pem
pkey=/etc/coturn/privkey.pem
EOF
systemctl restart coturn && ufw allow 5349/tcp

Add turns:turn.example.com:5349?transport=tcp to the urls list. Certificates are short-lived, so put the copy into a certbot deploy hook — otherwise TLS quietly breaks at the first renewal.

Worth knowing

  • TURN doesn't make a bad connection good. It adds one hop, so place the server near the people who need it.
  • If you run an SFU — Jitsi's videobridge, for example — that's a separate piece. TURN helps the clients that can't reach it directly.
  • The secret is the whole security model. Keep it out of the frontend, and rotate it if it ever leaks.

Ready to deploy? Pay with crypto, no KYC — live in about a minute.

Deploy now →

FAQ

Why do WebRTC calls need a TURN server at all?

WebRTC tries to connect peers directly, and usually succeeds. When both sides sit behind symmetric NAT or a firewall that drops UDP — hotel Wi-Fi, office networks, some mobile carriers — there's no direct path, and the call silently fails. A TURN server relays the media for exactly those calls; the commonly quoted share is somewhere around one call in ten.

Can TURN run on a NAT plan?

No. Clients connect in to UDP and TCP 3478 and to the relay port range, and a NAT plan only accepts your SSH port from outside. You need a plan with its own IPv4.

How much bandwidth does it use?

Every relayed stream passes through the server twice — in from one side, out to the other. A one-to-one video call at 1–2 Mbit/s per direction is a few Mbit/s on the server. CPU and RAM barely notice; the 1 Gbit/s port is the real ceiling.

Why not just put a fixed username and password in my app?

Because anything in browser JavaScript is public, and a TURN server with a known password is a free relay for the whole internet. With use-auth-secret your backend hands out credentials that expire, signed with a secret the browser never sees.

Do I need TLS on port 5349?

It helps on networks that block UDP and are strict about plain TCP. Add it once plain TURN works: give coturn a certificate for the TURN hostname and open 5349/tcp. We tested the relay over TLS as well.

Comments

No comments yet. Be the first.

Leave a comment

Comments are moderated before they appear.