Short version: you almost certainly do not need a UDP range at all. LiveKit supports a single-port UDP mux, and its embedded TURN server has no relay port range. Together those replace both of your ranges with three or four individual ports, which should fit whatever rule budget your infra team is working with.
1. Yes, this is a known Azure constraint, and it is worth naming precisely
If your LiveKit VM sits behind a public Azure Standard Load Balancer, each load-balancing rule maps a single frontend port. Azure’s HA Ports feature, which collapses all ports into one rule, is supported only on an internal Standard Load Balancer. Public load balancers and the retired Basic SKU do not support it. So a 10,000 port range would need 10,000 rules, which is not practical. That is very likely the “platform limitation on available port rules” your infra team hit.
Worth confirming which layer is actually blocking you, because the answer differs:
- If it is only an NSG: an NSG rule accepts a port range as a single rule, so
50000-60000 costs you one rule and there is no real limit. If NSG is the only thing in the path, nothing is stopping you from opening the full range.
- If it is a public Load Balancer: that is the hard constraint, and no amount of NSG tuning fixes it. Go to the UDP mux config below.
Two other Azure specifics to check while you are in there:
- Azure load-balancing rules do not forward IP fragments and do not support IP fragmentation of UDP or TCP packets. WebRTC generally keeps packets under MTU, but this bites some setups. Putting media on a public IP directly attached to the VM NIC, bypassing the LB entirely, avoids the whole class of problem.
- Your posted
rtc: block does not include use_external_ip: true. On Azure the VM sees only its private IP while clients need the public one, so without this (or an explicit node_ip) media will work from inside the VNet and fail for real users. This is by far the most common cause of “connects but no audio or video” on self-hosted deployments. If outbound UDP to public STUN is blocked in your environment, set node_ip to the VM’s public IP manually instead.
2. Are 200 / 448 ports reasonable for 20 to 30 participants?
By the raw math, yes. Each participant uses two ports, so 30 participants is about 60 ports and 200 gives you roughly 3x headroom.
But the math is misleading in two ways:
- It is per server, not per room. Three concurrent 30-person rooms is around 180 ports and you are at the edge. Five rooms and you are over.
- Ports are not freed the instant a participant leaves, and ICE restarts allocate before the old allocation is reclaimed. Your “stable ICE reconnects” requirement is exactly the workload that churns ports fastest. A reconnect storm after a brief network blip is when a tight range fails, and it fails as an unexplained inability to establish media rather than a clean error.
So: it will probably work today at your stated load, and it will fail in a confusing way at 3x. I would not ship it when the alternative below removes the constraint entirely rather than shrinking it.
3. The architecture I would actually recommend
Replace the media range with a single UDP mux port
rtc.udp_port handles all UDP traffic on one port. When it is set, port_range_start and port_range_end are not used and must be removed for it to take effect.
port: 7880
rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: true
# port_range_start and port_range_end MUST be deleted, not commented values
The documented tradeoff is that the mux becomes an overhead on highly trafficked deployments. At 20 to 30 concurrent participants on a single node you are nowhere near that. This is the standard configuration for single-instance self-hosted deployments and it is what I would run in your position.
If you want a little headroom without going back to a wide range, some deployments use a small mux range such as 7882-7892. A single port is fine at your scale.
Consider dropping Coturn for LiveKit’s embedded TURN
This is the part that eliminates your 448-port relay range. LiveKit’s embedded TURN server relays straight into the SFU, so its config has no relay port range at all. The entire turn: block is just ports:
turn:
enabled: true
domain: turn.yourdomain.com
udp_port: 443
tls_port: 5349
external_tls: true # set if an L4 LB or nginx stream block terminates TLS
# otherwise set external_tls: false and provide cert_file / key_file
Two constraints from the config reference:
turn.udp_port: only 53, 80 or 443 are permitted if below 1024. Port 443/UDP is the recommended choice, since firewalls increasingly pass UDP/443 for QUIC, and UDP relay beats TCP relay for congestion control and latency.
turn.tls_port: if you are not behind a load balancer this must be set to 443, because that is the value advertised to clients. Your current 5349 only works with an LB or proxy in front.
Note that UDP/443 and TCP/443 are independent, so TURN/UDP on 443 does not collide with nginx on TCP/443. It would collide if you later enable HTTP/3 on the same IP, so plan for that.
Resulting Azure rules
After both changes you need individual ports, no ranges:
| Port |
Proto |
Purpose |
| 443 |
TCP |
nginx, WSS signaling to LiveKit 7880 |
| 7882 |
UDP |
all WebRTC media (mux) |
| 7881 |
TCP |
ICE/TCP fallback |
| 443 |
UDP |
TURN/UDP relay (optional but recommended) |
| 5349 or 443 |
TCP |
TURN/TLS relay (optional) |
Three to five rules total. That should clear your infra team’s constraint with room to spare.
Important: rtc.tcp_port cannot sit behind a load balancer or TLS termination. It must be exposed directly on the node. WebRTC transports are already encrypted, so wrapping it adds nothing. Make sure nginx is not in front of 7881.
Verification
After the change, open chrome://webrtc-internals during a call and inspect the selected candidate pair. You want to see host or srflx candidates on udp/7882 winning. If everything is landing on relay or on tcp/7881, media is falling back and the mux port is not actually reachable, which usually means the NSG rule or the LB path is wrong rather than the LiveKit config.
For MatrixRTC and Element Call specifically, also confirm which ICE servers the client is actually being handed, since that can come from your homeserver’s VoIP config rather than from LiveKit. If Element Call is still being given the old Coturn URIs after you switch to embedded TURN, clients will keep trying to relay through a server whose ports you have closed.
References: