openai-cloudflare-deploy/references/realtime-sfu/gotchas.md
Version 49f948fa.bb1 · Apache-2.0. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.
← Return to resource and package checksum
Gotchas & Troubleshooting
Common Errors
"Slow initial connect (~1.8s)"
Cause: First STUN delayed during consensus forming (normal behavior) Solution: Subsequent connections are faster. CF detects DTLS ClientHello early to compensate.
"No media flow"
Cause: SDP exchange incomplete, connection not established, tracks not added before offer, browser permissions missing
Solution:
1. Verify SDP exchange complete
2. Check pc.connectionState === 'connected'
3. Ensure tracks added before creating offer
4. Confirm browser permissions granted
5. Use chrome://webrtc-internals for debugging
"Track not receiving"
Cause: Track not published, track ID not shared, session IDs mismatch, pc.ontrack not set, renegotiation needed
Solution:
1. Verify track published successfully
2. Confirm track ID shared between peers
3. Check session IDs match
4. Set pc.ontrack handler before answer
5. Trigger renegotiation if needed
"ICE connection failed"
Cause: Network changed, firewall blocked UDP, TURN needed, transient network issue Solution:
pc.oniceconnectionstatechange = async () => {
if (pc.iceConnectionState === 'failed') {
console.warn('ICE failed, attempting restart');
await pc.restartIce(); // Triggers new ICE gathering
// Create new offer with ICE restart flag
const offer = await pc.createOffer({iceRestart: true});
await pc.setLocalDescription(offer);
// Send to backend → Cloudflare API
await fetch(`/api/sessions/${sessionId}/renegotiate`, {
method: 'PUT',
body: JSON.stringify({sdp: offer.sdp})
});
}
};
"Track stuck/frozen"
Cause: Sender paused track, network congestion, codec mismatch, mobile browser backgrounded
Solution:
1. Check track.enabled and track.readyState === 'live'
2. Verify sender active: pc.getSenders().find(s => s.track === track)
3. Check stats for packet loss/jitter (see patterns.md)
4. On mobile: Re-acquire tracks when app foregrounded
5. Test with different codecs if persistent
"Network change disconnects call"
Cause: Mobile switching WiFi↔cellular, laptop changing networks Solution:
// Listen for network changes
if ('connection' in navigator) {
(navigator as any).connection.addEventListener('change', async () => {
console.log('Network changed');
await pc.restartIce(); // Use ICE restart pattern above
});
}
// Or use PartyTracks (handles automatically)
Retry with Exponential Backoff
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status >= 500) throw new Error('Server error');
return res; // Client error, don't retry
} catch (err) {
if (i === maxRetries - 1) throw err;
const delay = Math.min(1000 * 2 ** i, 10000); // Cap at 10s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Debugging with chrome://webrtc-internals
- Open
chrome://webrtc-internalsin Chrome/Edge - Find your PeerConnection in the list
- Check Stats graphs for packet loss, jitter, bandwidth
- Check ICE candidate pairs: Look for
succeededstate, relay vs host candidates - Check getStats: Raw metrics for inbound/outbound RTP
- Look for errors in Event log:
iceConnectionState,connectionStatechanges - Export data with "Download the PeerConnection updates and stats data" button
- Common issues visible here: ICE failures, high packet loss, bitrate drops
Limits
| Resource/Limit | Value | Notes |
|---|---|---|
| Egress (Free) | 1TB/month | Per account |
| Egress (Paid) | $0.05/GB | After free tier |
| Inbound traffic | Free | All plans |
| TURN service | Free | Included with SFU |
| Participants | No hard limit | Client bandwidth/CPU bound (typically 10-50 tracks) |
| Tracks per session | No hard limit | Client resources limited |
| Session duration | No hard limit | Production calls run for hours |
| WebRTC ports | UDP 1024-65535 | Outbound only, required for media |
| API rate limit | 600 req/min | Per app, burst allowed |
Security Checklist
- ✅ Never expose
CALLS_APP_SECRETto client - ✅ Validate user identity in backend before creating sessions
- ✅ Implement auth tokens for session access (JWT in custom header)
- ✅ Rate limit session creation endpoints
- ✅ Expire sessions server-side after inactivity
- ✅ Validate track IDs before subscribing (prevent unauthorized access)
- ✅ Use HTTPS for all signaling (API calls)
- ✅ Enable DTLS-SRTP (automatic with Cloudflare, encrypts media)
- ⚠️ Consider E2EE for sensitive content (implement client-side with Insertable Streams API)