Files
dotfiles/.agents/skills/cloudflare-deploy/references/turn/gotchas.md
2026-03-17 16:53:22 -07:00

6.9 KiB

TURN Gotchas & Troubleshooting

Common mistakes, security best practices, and troubleshooting for Cloudflare TURN.

Quick Reference

Issue Solution Details
Credentials not working Check TTL ≤ 48hrs See Troubleshooting
Connection drops after ~48hrs Implement credential refresh See Connection Drops
Port 53 fails in browser Filter server-side See Port 53
High packet loss Check rate limits See Rate Limits
Connection fails after maintenance Implement ICE restart See ICE Restart

Critical Constraints

Constraint Value Consequence if Violated
Max credential TTL 48 hours (172800s) API rejects request
Credential revocation delay ~seconds Billing stops immediately, connection drops shortly
IP allowlist update window 14 days (if IPs change) Connection fails if IPs change
Packet rate 5-10k pps per allocation Packet drops
Data rate 50-100 Mbps per allocation Packet drops
Unique IP rate >5 new IPs/sec Packet drops

Limits Per TURN Allocation

Per user (not account-wide):

  • IP addresses: >5 new unique IPs per second
  • Packet rate: 5-10k packets per second (inbound/outbound)
  • Data rate: 50-100 Mbps (inbound/outbound)
  • MTU: No specific limit
  • Burst rates: Higher than documented

Exceeding limits results in packet drops.

Common Mistakes

Setting TTL > 48 hours

// ❌ BAD: API will reject
const creds = await generate({ ttl: 604800 });  // 7 days

// ✅ GOOD:
const creds = await generate({ ttl: 86400 });   // 24 hours

Hardcoding IPs without monitoring

// ❌ BAD: IPs can change with 14-day notice
const iceServers = [{ urls: 'turn:141.101.90.1:3478' }];

// ✅ GOOD: Use DNS
const iceServers = [{ urls: 'turn:turn.cloudflare.com:3478' }];

Using port 53 in browsers

// ❌ BAD: Blocked by Chrome/Firefox
urls: ['turn:turn.cloudflare.com:53']

// ✅ GOOD: Filter port 53
urls: urls.filter(url => !url.includes(':53'))

Not handling credential expiry

// ❌ BAD: Credentials expire but call continues → connection drops
const creds = await fetchCreds();
const pc = new RTCPeerConnection({ iceServers: creds });

// ✅ GOOD: Refresh before expiry
setInterval(() => refreshCredentials(pc), 3000000);  // 50 min

Missing ICE restart support

// ❌ BAD: No recovery from TURN maintenance
pc.addEventListener('iceconnectionstatechange', () => {
  console.log('State changed:', pc.iceConnectionState);
});

// ✅ GOOD: Implement ICE restart
pc.addEventListener('iceconnectionstatechange', async () => {
  if (pc.iceConnectionState === 'failed') {
    await refreshCredentials(pc);
    pc.restartIce();
  }
});

Exposing TURN key secret client-side

// ❌ BAD: Secret exposed to client
const secret = 'your-turn-key-secret';
const response = await fetch(`https://rtc.live.cloudflare.com/v1/turn/...`, {
  headers: { 'Authorization': `Bearer ${secret}` }
});

// ✅ GOOD: Generate credentials server-side
const response = await fetch('/api/turn-credentials');

ICE Restart Required Scenarios

These events require ICE restart (see patterns.md):

  1. TURN server maintenance (occasional on Cloudflare's network)
  2. Network topology changes (anycast routing changes)
  3. Credential refresh during long sessions (>1 hour)
  4. Connection failure (iceConnectionState === 'failed')

Implement in all production apps:

pc.addEventListener('iceconnectionstatechange', async () => {
  if (pc.iceConnectionState === 'failed' || 
      pc.iceConnectionState === 'disconnected') {
    await refreshTURNCredentials(pc);
    pc.restartIce();
    const offer = await pc.createOffer({ iceRestart: true });
    await pc.setLocalDescription(offer);
    // Send offer to peer via signaling...
  }
});

Reference: RFC 8445 Section 2.4

Security Checklist

  • Credentials generated server-side only (never client-side)
  • TURN_KEY_SECRET in wrangler secrets, not vars
  • TTL ≤ expected session duration (and ≤ 48 hours)
  • Rate limiting on credential generation endpoint
  • Client authentication before issuing credentials
  • Credential revocation API for compromised sessions
  • No hardcoded IPs (or DNS monitoring in place)
  • Port 53 filtered for browser clients

Troubleshooting

Issue: TURN credentials not working

Check:

  • Key ID and secret are correct
  • Credentials haven't expired (check TTL)
  • TTL doesn't exceed 172800 seconds (48 hours)
  • Server can reach rtc.live.cloudflare.com
  • Network allows outbound HTTPS

Solution:

// Validate before using
if (ttl > 172800) {
  throw new Error('TTL cannot exceed 48 hours');
}

Issue: Slow connection establishment

Solutions:

  • Ensure proper ICE candidate gathering
  • Check network latency to Cloudflare edge
  • Verify firewall allows WebRTC ports (3478, 5349, 443)
  • Consider using TURN over TLS (port 443) for corporate networks

Issue: High packet loss

Check:

  • Not exceeding rate limits (5-10k pps)
  • Not exceeding bandwidth limits (50-100 Mbps)
  • Not connecting to too many unique IPs (>5/sec)
  • Client network quality

Issue: Connection drops after ~48 hours

Cause: Credentials expired (48hr max)

Solution:

  • Set TTL to expected session duration
  • Implement credential refresh with setConfiguration()
  • Use ICE restart if connection fails
// Refresh credentials before expiry
const refreshInterval = ttl * 1000 - 60000; // 1 min early
setInterval(async () => {
  await refreshTURNCredentials(pc);
}, refreshInterval);

Issue: Port 53 URLs in browser fail silently

Cause: Chrome/Firefox block port 53

Solution: Filter port 53 URLs server-side:

const filtered = urls.filter(url => !url.includes(':53'));

Issue: Hardcoded IPs stop working

Cause: Cloudflare changed IP addresses (14-day notice)

Solution:

  • Use DNS hostnames (turn.cloudflare.com)
  • Monitor DNS changes with automated alerts
  • Update allowlists within 14 days if using IP allowlisting

Cost Optimization

  1. Use appropriate TTLs (don't over-provision)
  2. Implement credential caching
  3. Set iceTransportPolicy: 'all' to try direct first (use 'relay' only when necessary)
  4. Monitor bandwidth usage
  5. Free when used with Cloudflare Calls SFU

See Also