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

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

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

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

1. Open `chrome://webrtc-internals` in Chrome/Edge
2. Find your PeerConnection in the list
3. Check **Stats graphs** for packet loss, jitter, bandwidth
4. Check **ICE candidate pairs**: Look for `succeeded` state, relay vs host candidates
5. Check **getStats**: Raw metrics for inbound/outbound RTP
6. Look for errors in **Event log**: `iceConnectionState`, `connectionState` changes
7. Export data with "Download the PeerConnection updates and stats data" button
8. 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_SECRET` to 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)
