Deploying a Production TURN Server: coturn Configuration Guide
A TURN server is the piece most WebRTC deployments underestimate until users behind symmetric NAT or enterprise firewalls start reporting failed calls. STUN handles roughly 80% of NAT traversal cases. The remaining 20% — corporate networks with strict egress filtering, CGNAT carriers, mobile networks with aggressive NAT — require a TURN relay. coturn is the de-facto open-source TURN implementation. This guide covers a production-grade deployment, not the five-line "it works on localhost" setup.
How TURN Fits Into WebRTC ICE
When two WebRTC peers connect, the ICE (Interactive Connectivity Establishment) process collects candidate addresses from three sources:
- Host candidates — the interface's own IP addresses
- Server-reflexive candidates — the public IP seen by a STUN server
- Relay candidates — IP:port pairs allocated on the TURN server
Relay candidates are the fallback. Media flows through the TURN server instead of peer-to-peer, adding latency (typically 20–60ms round-trip overhead) but guaranteeing connectivity. A TURN server handles both UDP and TCP relay, plus TLS-wrapped TCP (TURNS) for environments that block non-HTTP traffic.
Hardware Sizing
TURN relay is bandwidth-bound, not compute-bound. Each relayed call uses two UDP streams at the TURN server.
| Concurrent relayed calls | Bitrate per call | Required throughput | Recommended NIC |
|---|---|---|---|
| 100 | 100 Kbps audio | 20 Mbps | 100 Mbps |
| 500 | 100 Kbps audio | 100 Mbps | 1 Gbps |
| 1,000 | 500 Kbps video | 1 Gbps | 10 Gbps |
| 5,000 | 500 Kbps video | 5 Gbps | 10 Gbps bonded |
CPU usage is negligible — coturn uses minimal processing per relay allocation. Memory is ~4 KB per active allocation. A $40/month VPS with a 1 Gbps NIC handles 500 concurrent relayed calls comfortably.
Installing coturn
# Debian / Ubuntu
apt-get install coturn
# Enable the service
sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
Core Configuration (/etc/turnserver.conf)
# Network
listening-port=3478
tls-listening-port=5349
listening-ip=0.0.0.0
relay-ip=YOUR_PUBLIC_IP
external-ip=YOUR_PUBLIC_IP
# Authentication — use long-term credential mechanism
lt-cred-mech
realm=turn.example.com
# TLS — use a real cert, not self-signed
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
cipher-list="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
no-sslv3
no-tlsv1
no-tlsv1_1
# Database for credentials
userdb=/var/lib/turn/turndb
# Logging
log-file=/var/log/coturn/turnserver.log
verbose
# Security hardening
no-loopback-peers
no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
# Quota enforcement
user-quota=10
total-quota=1000
max-bps=500000
# Prometheus metrics
prometheus
prometheus-port=9641
The denied-peer-ip directives are critical for security. Without them, an attacker can use your TURN server to relay traffic to internal RFC 1918 addresses — effectively using it as a proxy to reach your private network. Always block private ranges.
Credential Management
coturn uses SQLite by default. For multi-server deployments, switch to PostgreSQL or Redis so all nodes share the same credential store.
# Add a static user (for testing only)
turnadmin -a -u testuser -r turn.example.com -p secretpass
# Generate a time-limited credential (for production)
# HMAC-SHA1 of "timestamp:username" with your shared secret
For production WebRTC applications, use the REST API credential pattern. Your application server generates short-lived TURN credentials on demand:
import hmac, hashlib, base64, time
def generate_turn_credentials(username, secret, ttl=3600):
timestamp = int(time.time()) + ttl
turn_user = f"{timestamp}:{username}"
dig = hmac.new(
secret.encode(),
turn_user.encode(),
hashlib.sha1
).digest()
password = base64.b64encode(dig).decode()
return {"username": turn_user, "password": password}
Configure coturn to validate these credentials:
use-auth-secret
static-auth-secret=YOUR_SHARED_SECRET
This way, TURN credentials expire automatically (the timestamp is baked in) and you never store user passwords in the TURN database. A compromised TURN credential is useless after TTL seconds.
Firewall Rules
# Required ports
ufw allow 3478/udp # STUN/TURN
ufw allow 3478/tcp # TURN over TCP
ufw allow 5349/tcp # TURNS (TLS)
ufw allow 5349/udp # TURNS (DTLS)
# Relay port range — must match coturn config
ufw allow 49152:65535/udp
# Prometheus scrape (from monitoring server only)
ufw allow from MONITOR_IP to any port 9641
The relay port range (49152–65535) is where coturn allocates relay endpoints. Each active TURN allocation uses one port from this range. Size the range to at least total-quota * 2 ports.
Prometheus Metrics and Alerting
coturn exposes Prometheus metrics on port 9641 when prometheus is set in the config.
Key metrics to alert on:
| Metric | Alert threshold | Meaning |
|---|---|---|
coturn_total_allocations_quota_exceeded_total | > 0 per minute | Users hitting quota limits |
coturn_current_allocations | > 80% of total-quota | Approaching capacity |
coturn_total_traffic_bytes | Rate > 90% of NIC capacity | Bandwidth saturation |
coturn_errors_total | Spike | Auth failures / attacks |
# prometheus/rules/coturn.yml
groups:
- name: coturn
rules:
- alert: TurnCapacityHigh
expr: coturn_current_allocations / 1000 > 0.8
for: 2m
labels:
severity: warning
annotations:
summary: "TURN server at {{ $value | humanizePercentage }} capacity"
- alert: TurnBandwidthSaturated
expr: rate(coturn_total_traffic_bytes[1m]) > 900000000
for: 1m
labels:
severity: critical
Multi-Region Deployment
A single TURN server creates a single point of failure and adds latency for distant users. Deploy TURN servers in each region where your users are concentrated, and use GeoDNS or anycast to route ICE candidates to the nearest node.
Application server pattern:
// Return region-appropriate TURN servers based on user location
function getIceServers(userRegion) {
const servers = {
'us-east': 'turn-us-east.example.com',
'eu-west': 'turn-eu-west.example.com',
'ap-southeast': 'turn-ap.example.com',
};
const primary = servers[userRegion] || servers['us-east'];
return [
{ urls: `stun:${primary}:3478` },
{
urls: [`turn:${primary}:3478`, `turns:${primary}:5349`],
username: credentials.username,
credential: credentials.password,
},
];
}
Always include at least two TURN server URLs in the ICE configuration — UDP primary and TCP/TLS fallback. Browsers automatically fall through to TCP and then TLS when UDP is blocked.
Operational Checks
After deployment, validate with a TURN test client before sending production traffic:
# Install turnutils (comes with coturn)
turnutils_uclient -t -u testuser -w secretpass -p 3478 turn.example.com
# Check allocation success rate — should be 100%
# Check round-trip time — should be < 5ms on same-region VPS
A healthy TURN server shows allocation latency under 5ms and zero auth failures in the coturn log. If you see 401 Unauthorized in logs, check that your application server's shared secret matches the static-auth-secret in turnserver.conf exactly — whitespace differences cause silent mismatches.




