# 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](#issue-turn-credentials-not-working)     |
| Connection drops after ~48hrs      | Implement credential refresh | [See Connection Drops](#issue-connection-drops-after-48-hours) |
| Port 53 fails in browser           | Filter server-side           | [See Port 53](#using-port-53-in-browsers)                      |
| High packet loss                   | Check rate limits            | [See Rate Limits](#limits-per-turn-allocation)                 |
| Connection fails after maintenance | Implement ICE restart        | [See ICE Restart](#ice-restart-required-scenarios)             |

## 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

```typescript
// ❌ 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

```typescript
// ❌ 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

```typescript
// ❌ 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

```typescript
// ❌ 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

```typescript
// ❌ 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

```typescript
// ❌ 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](./patterns.md#ice-restart-pattern)):

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:

```typescript
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](https://datatracker.ietf.org/doc/html/rfc8445#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:**

```typescript
// 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

```typescript
// 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:

```typescript
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

- [api.md](./api.md) - Credential generation API, revocation
- [configuration.md](./configuration.md) - IP allowlisting, monitoring
- [patterns.md](./patterns.md) - ICE restart, credential refresh patterns
