# TURN Implementation Patterns Production-ready patterns for implementing Cloudflare TURN in WebRTC applications. ## Prerequisites Before implementing these patterns, ensure you have: - TURN key created: see [api.md#create-turn-key](./api.md#create-turn-key) - Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration) ## Basic TURN Configuration (Browser) ```typescript interface RTCIceServer { urls: string | string[]; username?: string; credential?: string; credentialType?: "password" | "oauth"; } async function getTURNConfig(): Promise { const response = await fetch('/api/turn-credentials'); const data = await response.json(); return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: [ 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp', 'turns:turn.cloudflare.com:443?transport=tcp' ], username: data.username, credential: data.credential, credentialType: 'password' } ]; } // Use in RTCPeerConnection const iceServers = await getTURNConfig(); const peerConnection = new RTCPeerConnection({ iceServers }); ``` ## Port Selection Strategy Recommended order for browser clients: 1. **3478/udp** (primary, lowest latency) 2. **3478/tcp** (fallback for UDP-blocked networks) 3. **5349/tls** (corporate firewalls, most reliable) 4. **443/tls** (alternate TLS port, firewall-friendly) **Avoid port 53**—blocked by Chrome and Firefox. ```typescript function filterICEServersForBrowser(urls: string[]): string[] { return urls .filter(url => !url.includes(':53')) // Remove port 53 .sort((a, b) => { // Prioritize UDP over TCP over TLS if (a.includes('transport=udp')) return -1; if (b.includes('transport=udp')) return 1; if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1; if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1; return 0; }); } ``` ## Credential Refresh (Mid-Session) When credentials expire during long calls: ```typescript async function refreshTURNCredentials(pc: RTCPeerConnection): Promise { const newCreds = await fetch('/turn-credentials').then(r => r.json()); const config = pc.getConfiguration(); config.iceServers = newCreds.iceServers; pc.setConfiguration(config); // Note: setConfiguration() does NOT trigger ICE restart // Combine with restartIce() if connection fails } // Auto-refresh before expiry setInterval(async () => { await refreshTURNCredentials(peerConnection); }, 3000000); // 50 minutes if TTL is 1 hour ``` ## ICE Restart Pattern After network change, TURN server maintenance, or credential expiry: ```typescript pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed') { console.warn('ICE connection failed, restarting...'); // Refresh credentials await refreshTURNCredentials(pc); // Trigger ICE restart pc.restartIce(); const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); // Send offer to peer via signaling channel... } }); ``` ## Credentials Caching Pattern ```typescript class TURNCredentialsManager { private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null; async getCredentials(keyId: string, keySecret: string): Promise { const now = Date.now(); if (this.creds && this.creds.expiresAt > now) { return this.buildIceServers(this.creds); } const ttl = 3600; if (ttl > 172800) throw new Error('TTL max 48hrs'); const res = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ttl }) } ); const data = await res.json(); const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53')); this.creds = { username: data.iceServers.username, credential: data.iceServers.credential, urls: filteredUrls, expiresAt: now + (ttl * 1000) - 60000 }; return this.buildIceServers(this.creds); } private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] { return [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const } ]; } } ``` ## Common Use Cases ```typescript // Video conferencing: TURN as fallback const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' }; // IoT/predictable connectivity: force TURN const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' }; // Screen sharing: reduce overhead const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' }); ``` ## Integration with Cloudflare Calls SFU ```typescript // TURN is automatically used when needed // Cloudflare Calls handles TURN + SFU coordination const session = await callsClient.createSession({ appId: 'your-app-id', sessionId: 'meeting-123' }); ``` ## Debugging ICE Connectivity ```typescript pc.addEventListener('icecandidate', (event) => { if (event.candidate) { console.log('ICE candidate:', event.candidate.type, event.candidate.protocol); } }); pc.addEventListener('iceconnectionstatechange', () => { console.log('ICE state:', pc.iceConnectionState); }); // Check selected candidate pair const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'candidate-pair' && report.selected) { console.log('Selected:', report); } }); ``` ## See Also - [api.md](./api.md) - Credential generation API, types - [configuration.md](./configuration.md) - Worker setup, environment variables - [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting