Before going further: @Meharab_Islam_Nibir is self-hosted, so the LiveKit Cloud dashboard, session events, agent insights and project ID do not exist for this deployment. Worth redirecting to server logs, otherwise this round trip repeats.
Two corrections, then the test that should settle it.
“RemoveParticipant is being called” is probably a symptom, not the cause
livekit-server runs its own internal participant-teardown routine on every disconnect, whatever triggered it: client left, websocket died, media timed out, identity collision, room closed. So seeing it fire repeatedly tells you participants are leaving repeatedly. It does not tell you that anything called the API.
The title of this thread may be pointing you at your backend when the problem is elsewhere.
Test 1: is anything actually hitting the API?
RemoveParticipant is a Twirp HTTP endpoint:
POST /twirp/livekit.RoomService/RemoveParticipant
If you have nginx, Caddy or any proxy in front of port 7880, grep its access log:
grep 'twirp/livekit.RoomService' access.log
- Zero hits → nothing external is calling it. Stop looking at your backend entirely.
- Hits → the access log gives you source IP and User-Agent, which identifies the caller directly.
This takes one minute and rules out half the possibilities.
Test 2: get the disconnect reason (this is the real answer)
Every disconnect carries a numeric reason. In Flutter, read reason off RoomDisconnectedEvent, and disconnectReason on the participant. From the protocol definition:
| # |
Reason |
Meaning |
| 2 |
DUPLICATE_IDENTITY |
another participant with the same identity joined |
| 4 |
PARTICIPANT_REMOVED |
RoomService.RemoveParticipant was called |
| 5 |
ROOM_DELETED |
RoomService.DeleteRoom was called |
| 6 |
STATE_MISMATCH |
client resuming a session the server has no record of |
| 9 |
SIGNAL_CLOSE |
the signal websocket closed unexpectedly |
| 10 |
ROOM_CLOSED |
all standard and ingress participants left |
| 14 |
CONNECTION_TIMEOUT |
server timed out the participant session |
| 15 |
MEDIA_FAILURE |
media stream failure or media timeout |
That single integer picks your investigation for you:
4 → an API call really is happening. Test 1 finds the caller.
2 → duplicate identity, which was a good guess earlier in the thread. The Flutter-specific version: a widget rebuild constructs a second Room and connects with the same token. Each connection evicts the other, the evicted SDK auto-reconnects, and you get a ping-pong that only ends when the room empties. Check that your Room is created once and held outside the rebuild path, and that identity is unique per session rather than per user.
6 → this is the classic self-hosted killer. It usually means more than one livekit-server replica without a shared Redis, or a load balancer that isn’t sticky. A reconnect lands on a node that has never heard of the session. Symptom profile matches yours exactly: works fine for a couple of minutes, then falls apart.
9 → your reverse proxy is cutting the websocket. nginx proxy_read_timeout defaults to 60 seconds and applies to idle websockets. You need a long proxy_read_timeout and proxy_send_timeout, plus proxy_http_version 1.1 with the Upgrade and Connection headers. “2 to 3 minutes” is suspiciously close to a couple of failed reconnect cycles on a 60 second timer.
14 or 15 → media never established, or died. On self-hosted this is nearly always use_external_ip not set, or the UDP media ports not open. Signalling connects, the call looks alive briefly, then the server times the session out.
10 → worth understanding even if it isn’t your code: a room closes when the last non-agent participant leaves. An agent sitting alone does not hold a room open. So if your human participant drops for any reason above, the room closing afterwards is the consequence, not the bug.
Check the ordering
In the server log, confirm whether the participant leaves first and the room closes after, or the reverse. Timestamps settle it. My expectation given your description is participant drop first, room close second, which would mean the room closing is a red herring and the real question is why the participant dropped.
If you are running LiveKit Agents
Check your agent worker logs at the same timestamps. Some agent shutdown paths call delete_room, and some starter templates do it when the job ends. If the agent job dies at the 2 to 3 minute mark (unhandled exception, worker restart, redeploy) and the shutdown handler deletes the room, that is a genuine phantom API call from code you did not write. It would show up as reason 5.
What to post to get a real answer
- livekit-server version, and replica count
- Is Redis configured, and shared across all replicas?
- What sits in front of 7880, and its timeout settings
- Your
rtc config block, specifically use_external_ip and port settings
- The disconnect reason integer from Test 2
- The result of Test 1
- 30 seconds of server log at
level: debug around one failure, with participant identity and sid visible
- Confirmation that every participant has a distinct identity
With the reason code and the twirp grep, this becomes a five minute diagnosis instead of a guessing game.