Hey Faraan,
Darryns answer is spot on. I accidently duplicated effort here and had also looked at this, so will share my perspective on it as well since I already looked at it. I hope one of our answers will help you.
Also, thanks for the detailed writeup, the telemetry you’ve collected (loss spikes on host/prflx, multi-second RTT, forced TURN/TLS fallback) paints a very recognizable picture of GCC access networks. Good news up front: you’re closer to the right setup than you think, and the biggest lever available to you doesn’t require the Scale plan. Taking your three questions in order.
1. Do you need to manually target the me region?
Short answer: no — and manually rewriting the URL isn’t the supported way to do it anyway.
LiveKit Cloud routes every connection to the nearest healthy edge automatically, on every plan. When the SDK connects, it first hits a region-discovery endpoint that returns the closest regions to the client’s IP, sorted by proximity, and connects to the best one — falling back to the next-closest if that connection fails. Your Saudi and UAE students should already be landing on the me edges (Jeddah/Dubai) today without you doing anything.
You can verify this directly instead of guessing — after connect, log:
room.once(RoomEvent.Connected, () => {
console.log('edge region:', room.serverInfo?.region);
});
Add that to the telemetry you’re already collecting. If GCC users are consistently landing somewhere other than me, that’s genuinely unexpected and worth a thread of its own with a few example session IDs.
Region pinning (the Scale-plan feature) solves the opposite problem: it restricts which regions may serve your project, for compliance/data-residency, and in doing so turns off failover to other regions. It doesn’t make routing “more local” than the default — the default is already nearest-edge.
One important caveat on expectations: connecting to a local edge shortens the network path and helps RTT, but the packet mangling you’re measuring on host/prflx candidates is happening in the student’s access network — the ISP’s DPI is throttling UDP before it ever leaves the country. A Jeddah edge doesn’t exempt that traffic from inspection. So this reduces cross-border latency, but it won’t fix the UDP story on its own. That’s what #2 is for.
2. TURN best practices for DPI-heavy networks
What LiveKit Cloud already does: TURN/TLS on *.turn.livekit.cloud is served on TCP 443 with a real TLS handshake and a valid certificate. To port-based and protocol-based DPI, that connection is indistinguishable from ordinary HTTPS — there’s no separate “masking” toggle because real TLS on 443 is the masking. (Like any HTTPS connection, the SNI is visible, so an ISP that block-lists *.turn.livekit.cloud by hostname could still interfere — but that’s not a pattern we’ve seen deployed broadly in GCC networks.)
The actual best practice for your situation is to stop letting badly-throttled users attempt UDP at all. When you can identify a user as being on a hostile network (geo-IP, ASN, or a failed prior session), force relay from the start:
await room.connect(url, token, {
rtcConfig: { iceTransportPolicy: 'relay' },
});
This does three things for you:
- Faster, more reliable setup — ICE doesn’t spend time probing host/srflx paths that show 40–80% loss.
- No mid-call “UDP whack” — the classic DPI pattern is a UDP flow that works for 30–90 seconds until it’s classified, then gets throttled to death, forcing a disruptive mid-call migration. Starting on relay avoids ever being on that path.
- Deterministic behavior you can reason about in telemetry, instead of a different ICE outcome per student per session.
Note that 'relay' still allows TURN/UDP where it works — the browser will pick the best relay transport, and falls back to TURN/TLS 443 when UDP is blocked outright. You don’t need (and can’t currently force) TLS-only.
For triage, have affected students run the LiveKit connection test — it exercises each transport in order (WebSocket, WebRTC/UDP, TURN, TCP-vs-UDP, closest region) and tells you exactly which rungs of the ladder their network allows. The same checks are available programmatically via the ConnectionCheck class in livekit-client, so you can build a pre-call network check right into your tutoring app:
flowchart TD
A[ICE / UDP 50000-60000] -->|blocked or throttled| B[TURN/UDP]
B -->|UDP blocked entirely| C[TURN/TLS on TCP 443<br/>looks like HTTPS to DPI]
A -->|clean path| D[Direct media âś…]
B -->|works| D
C --> D
3. Client-side settings for high-loss / forced-TCP calls
A few things, roughly in order of impact for a 1:1 tutoring app:
Keep RED on (it’s on by default). livekit-client enables redundant audio encoding (RED) by default for mono Opus tracks — each packet carries a copy of earlier audio, so isolated UDP loss is nearly free to recover from. Just make sure nothing in your publish options sets red: false. Same for dtx, also on by default.
On forced-TCP paths, the enemy is queue depth, not loss. TCP retransmits everything, so “loss” surfaces as latency spikes that overflow the browser’s jitter buffer — which you can’t tune from the SDK. What you can control is how full the pipe is: a TCP connection running near its bandwidth limit turns every loss event into a multi-hundred-ms stall for the audio riding behind it. Concretely:
- Cap video aggressively for relay/TCP users — for 1:1 tutoring you don’t need much. Set a conservative
videoEncoding.maxBitrate, or drop to audio-only when quality degrades:
room.on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
if (participant.isLocal && quality === ConnectionQuality.Poor) {
room.localParticipant.setCameraEnabled(false); // protect the audio
}
});
- A lower
audioPreset (e.g. AudioPresets.speech instead of the default) buys headroom on constrained links with little perceptual cost for voice.
- Keep
adaptiveStream: true so the subscribe side downshifts automatically.
On the echo cancellation failures: AEC breaking is usually downstream of choppy far-end playback — the canceller can’t model a reference signal full of gaps. In our experience you don’t fix AEC directly; you fix delivery (everything above) and AEC recovers with it.
If you instrument room.serverInfo?.region plus per-session iceTransportPolicy and connection-quality events, you’ll very quickly see which combination (local edge + forced relay + capped video) moves your GCC metrics. Happy to dig into specific session traces if you get stuck — a couple of room names/SIDs from bad sessions would be enough.
Some Sresource: Region pinning docs, Regions overview, Firewall configuration, Firewall troubleshooting KB, Connection test KB, Codecs & RED docs,