Production deployment on Azure with Load Balancer UDP port limitations

Hi LiveKit Team,

We are deploying a self-hosted LiveKit server (v1.13.4) in a production environment on Azure and would appreciate your advice regarding an infrastructure limitation.

Environment

  • Self-hosted LiveKit using Docker
  • Separate Coturn server
  • Azure VM behind Azure networking infrastructure
  • Nginx reverse proxy
  • MatrixRTC / Element Call

Current configuration

LiveKit

rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 50199

Coturn

  • TCP/UDP 3478
  • TCP 5349
  • UDP 49152–49599 (relay)

Background

The official documentation recommends much larger UDP ranges for both LiveKit media and TURN relay.

However, our infrastructure team informed us that, due to the current Azure networking design, they cannot expose the full recommended UDP ranges because of platform/architecture limitations on the available port rules.

As a result, we had to significantly reduce the media and relay port ranges.

Questions

  1. Have you encountered similar Azure deployment limitations?

  2. Are the following ranges reasonable for a production deployment supporting approximately 20–30 concurrent participants with:

    • Audio
    • Video
    • Screen sharing
    • Stable ICE reconnects
  • LiveKit media: UDP 50000–50199 (200 ports)
  • TURN relay: UDP 49152–49599 (448 ports)
  1. If these ranges are still not recommended, what production architecture would you suggest for environments where the infrastructure cannot expose the default UDP ranges?

We understand that the networking implementation is infrastructure-specific. Our goal is to understand the recommended LiveKit deployment architecture and networking requirements when the infrastructure cannot expose the default recommended UDP ranges.

Thank you in advance for your guidance.

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: