Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/realtime-sfu/gotchas.md
1# Gotchas & Troubleshooting23## Common Errors45### "Slow initial connect (~1.8s)"67**Cause:** First STUN delayed during consensus forming (normal behavior)8**Solution:** Subsequent connections are faster. CF detects DTLS ClientHello early to compensate.910### "No media flow"1112**Cause:** SDP exchange incomplete, connection not established, tracks not added before offer, browser permissions missing13**Solution:**141. Verify SDP exchange complete152. Check `pc.connectionState === 'connected'`163. Ensure tracks added before creating offer174. Confirm browser permissions granted185. Use `chrome://webrtc-internals` for debugging1920### "Track not receiving"2122**Cause:** Track not published, track ID not shared, session IDs mismatch, `pc.ontrack` not set, renegotiation needed23**Solution:**241. Verify track published successfully252. Confirm track ID shared between peers263. Check session IDs match274. Set `pc.ontrack` handler before answer285. Trigger renegotiation if needed2930### "ICE connection failed"3132**Cause:** Network changed, firewall blocked UDP, TURN needed, transient network issue33**Solution:**34```typescript35pc.oniceconnectionstatechange = async () => {36if (pc.iceConnectionState === 'failed') {37console.warn('ICE failed, attempting restart');38await pc.restartIce(); // Triggers new ICE gathering3940// Create new offer with ICE restart flag41const offer = await pc.createOffer({iceRestart: true});42await pc.setLocalDescription(offer);4344// Send to backend → Cloudflare API45await fetch(`/api/sessions/${sessionId}/renegotiate`, {46method: 'PUT',47body: JSON.stringify({sdp: offer.sdp})48});49}50};51```5253### "Track stuck/frozen"5455**Cause:** Sender paused track, network congestion, codec mismatch, mobile browser backgrounded56**Solution:**571. Check `track.enabled` and `track.readyState === 'live'`582. Verify sender active: `pc.getSenders().find(s => s.track === track)`593. Check stats for packet loss/jitter (see patterns.md)604. On mobile: Re-acquire tracks when app foregrounded615. Test with different codecs if persistent6263### "Network change disconnects call"6465**Cause:** Mobile switching WiFi↔cellular, laptop changing networks66**Solution:**67```typescript68// Listen for network changes69if ('connection' in navigator) {70(navigator as any).connection.addEventListener('change', async () => {71console.log('Network changed');72await pc.restartIce(); // Use ICE restart pattern above73});74}7576// Or use PartyTracks (handles automatically)77```7879## Retry with Exponential Backoff8081```typescript82async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {83for (let i = 0; i < maxRetries; i++) {84try {85const res = await fetch(url, options);86if (res.ok) return res;87if (res.status >= 500) throw new Error('Server error');88return res; // Client error, don't retry89} catch (err) {90if (i === maxRetries - 1) throw err;91const delay = Math.min(1000 * 2 ** i, 10000); // Cap at 10s92await new Promise(resolve => setTimeout(resolve, delay));93}94}95}96```9798## Debugging with chrome://webrtc-internals991001. Open `chrome://webrtc-internals` in Chrome/Edge1012. Find your PeerConnection in the list1023. Check **Stats graphs** for packet loss, jitter, bandwidth1034. Check **ICE candidate pairs**: Look for `succeeded` state, relay vs host candidates1045. Check **getStats**: Raw metrics for inbound/outbound RTP1056. Look for errors in **Event log**: `iceConnectionState`, `connectionState` changes1067. Export data with "Download the PeerConnection updates and stats data" button1078. Common issues visible here: ICE failures, high packet loss, bitrate drops108109## Limits110111| Resource/Limit | Value | Notes |112|----------------|-------|-------|113| Egress (Free) | 1TB/month | Per account |114| Egress (Paid) | $0.05/GB | After free tier |115| Inbound traffic | Free | All plans |116| TURN service | Free | Included with SFU |117| Participants | No hard limit | Client bandwidth/CPU bound (typically 10-50 tracks) |118| Tracks per session | No hard limit | Client resources limited |119| Session duration | No hard limit | Production calls run for hours |120| WebRTC ports | UDP 1024-65535 | Outbound only, required for media |121| API rate limit | 600 req/min | Per app, burst allowed |122123## Security Checklist124125- ✅ **Never expose** `CALLS_APP_SECRET` to client126- ✅ **Validate user identity** in backend before creating sessions127- ✅ **Implement auth tokens** for session access (JWT in custom header)128- ✅ **Rate limit** session creation endpoints129- ✅ **Expire sessions** server-side after inactivity130- ✅ **Validate track IDs** before subscribing (prevent unauthorized access)131- ✅ **Use HTTPS** for all signaling (API calls)132- ✅ **Enable DTLS-SRTP** (automatic with Cloudflare, encrypts media)133- ⚠️ **Consider E2EE** for sensitive content (implement client-side with Insertable Streams API)134