Skip to content
Deploying a Production TURN Server: coturn Configuration Guide
WebRTC

Deploying a Production TURN Server: coturn Configuration Guide

Step-by-step coturn configuration for production WebRTC: TLS setup, credential management, quota enforcement, Prometheus metrics, and multi-region deployment patterns.

Tumarm Engineering8 min read

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:

  1. Host candidates — the interface's own IP addresses
  2. Server-reflexive candidates — the public IP seen by a STUN server
  3. 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 callsBitrate per callRequired throughputRecommended NIC
100100 Kbps audio20 Mbps100 Mbps
500100 Kbps audio100 Mbps1 Gbps
1,000500 Kbps video1 Gbps10 Gbps
5,000500 Kbps video5 Gbps10 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:

MetricAlert thresholdMeaning
coturn_total_allocations_quota_exceeded_total> 0 per minuteUsers hitting quota limits
coturn_current_allocations> 80% of total-quotaApproaching capacity
coturn_total_traffic_bytesRate > 90% of NIC capacityBandwidth saturation
coturn_errors_totalSpikeAuth 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.

coturnturn-serverwebrtcicestunnat-traversal
Benchmark
BALI Pvt.Ltd
Brave BPO
Wave
SmartBrains BPO

Ready to build on carrier-grade voice?

Talk to a VoIP engineer — not a salesperson.