Hello LiveKit community,
I’m self-hosting LiveKit via Docker Compose and load-testing with the lk CLI. At 1,000 simulated participants, 189 of 975 subscriber connections (~19%) failed with:
could not connect Sub 808: could not establish signal connection
Around the same timestamps, the TURN client logs show:
turnc ERROR: Fail to refresh permissions: all retransmissions failed for <participant>
turnc ERROR: Fail to refresh permissions: transaction closed
Failures are concentrated among the later-joining participants (roughly sub #794–999, the tail of the ramp-up), not spread evenly across all 1,000 consistent with a connection-burst/timing issue rather than a hard capacity ceiling.
Server resources were not saturated during the failure window — docker stats sampled every 2s for the whole run shows the LiveKit container peaking at 482.93% CPU (of 800% / 8 cores, ~60%) and 2.8 GiB / 7.75 GiB RAM. Redis and the API container also stayed well under load in comparable runs. This is why I don’t think this is a raw CPU/RAM/FD-limit problem, but I’d like the community’s read on it.
Environment
- Deployment: Self-hosted, Docker Compose, single host
- LiveKit server:
livekit/livekit-server:v1.11.0 - LiveKit CLI (
lk): v2.16.7 - Server: 8 cores / 7.8 GB RAM
- Redis: 7.4.8 (single instance,
redis:7-alpine) - Reverse proxy: Nginx (LiveKit itself runs with
network_mode: host, not behind Docker bridge NAT) - TURN/TLS: enabled (
domain,tls_port: 5349, cert configured inlivekit.yaml) - Load-test command:
lk load-test \
--url wss://<my-livekit-domain> \
--api-key $LIVEKIT_API_KEY \
--api-secret $LIVEKIT_API_SECRET \
--room stepped-loadtest-1000 \
--audio-publishers 5 \
--subscribers 995 \
--num-per-second 10 \
--duration 160s
(ramp rate: 10 participants/sec, so ~1000 participants joining over ~100s)
Known confound: the load-test client runs on the same 8 cores as the server under test, so part of the load at the moment of failure is the test tool’s own CPU cost (invisible to docker stats), not purely server-side. A second-machine retest is on my list but hasn’t happened yet.
Expected behavior
All 1,000 simulated participants connect, or the failures scale roughly proportionally with load rather than clustering sharply among the last-joining participants.
Actual behavior
189/975 subscribers failed with could not establish signal connection, clustered in the tail of the ramp, alongside TURN “fail to refresh permissions” errors on the same connections around the same timestamps.
Questions
- Is “Fail to refresh permissions: transaction closed / all retransmissions failed” a downstream symptom of the signaling/ICE connection already failing or timing out, or could TURN allocation itself be the root cause of the signal-connection failure? (My working assumption is the former the transaction closes because the underlying connection attempt failed, but I’d like to confirm.)
- Does
Sub 808etc. correspond directly to the Nth simulated subscriber inlk load-test’s connection order? - Given CPU/RAM headroom, is there a known per-node concurrency limit on simultaneous ICE/DTLS handshakes (vs. steady-state media forwarding) that a connection burst could hit even while average resource usage looks fine?
- Is
--num-per-second 10too aggressive for a 1,000-participant single-room ramp? Is there a recommended ramp rate/burst size for large-scalelk load-testruns? - Are there Prometheus metrics or debug logs specifically for ICE/TURN handshake latency or signal-connection setup time (as opposed to steady-state track stats) that would help pinpoint where in the handshake this is timing out?
Thank you.