Self-hosted LiveKit rooms automatically closing after 2-3 minutes (Phantom API calls from backend)

I am experiencing an unexpected issue with my self-hosted LiveKit server. I am using a Flutter app for the frontend and a Laravel backend for room management and token generation.

To be honest, I don’t know exactly which code is causing the problem, which is why I can’t provide a minimal reproducible example (MRE). It seems to be a strange production environment issue rather than a simple code bug.

The Issue: My app ran perfectly in production for 4 months without any issues. However, for the past 2 days, any newly created room only stays active for about 2-3 minutes. After this short time, all participants are forcefully removed/disconnected, and the room automatically closes.

What I have tried & Log observations:

  1. I completely wiped my VPS, re-uploaded my backend code, and did a fresh installation of the LiveKit server.

  2. After the fresh install, everything worked perfectly for about 10-12 hours. But after that period, the exact same issue returned.

  3. I investigated the LiveKit logs and found that API requests for ‘remove participant’ and ‘close room’ are being triggered from my backend.

  4. The confusing part: My Laravel backend has absolutely NO logic or endpoints written by me that trigger these specific API calls to remove participants or close rooms.

Expected behavior: The room should remain active as long as there are connected participants or until it is explicitly closed via the backend API. Participants should not be randomly disconnected after a few minutes.

Steps to reproduce:

  1. Create a room via the Laravel backend.

  2. Join the room using the Flutter livekit_client SDK.

  3. Wait for 2-3 minutes.

  4. Observe that all participants are suddenly disconnected and the room is closed on the server.

Since the logs show these calls coming from my backend, but my codebase doesn’t initiate them, how can I trace what is making these phantom calls? Could this be related to a token expiration, a background worker, or a compromised server issue? Any guidance on debugging this would be highly appreciated.

@Meharab_Islam_Nibir I’m not sure what’s really causing this on your end. But these are the tips I can offer you.

  1. Analyze your session in Livekit Dashbaord. Look into the session events and agent insights and see what events you’re getting. This will give you a good idea of what’s happening.
  2. Test your agent in Livekit console to fully check that the agent is working as expected. This helps you isolate the agent from the frontend and see if the agent alone is working as expected. And then analyze the session events and agents insights of a session initiated by your flutter frontend vs one initiated from Livekit console.
  3. Check your VPS logs as well.
  4. How many participants join your typical room? Ensure that no two participants share the same participant_identity as this will cause duplication.
  5. If the above points don’t help you to find the issue, then provide your Livekit project ID and your specific session ID that’s having this issue. A livekit staff might help you do further investigation.

I hope this helps.

“Thanks for the tips! I checked the logs as you suggested and noticed something strange: the remove participant function is being called repeatedly. However, I haven’t implemented any ‘remove participant’ logic or feature in either my Flutter app or my backend code. Any idea what could be triggering this automatically?”

I’m glad the tips I have provided were helpful.

Any idea what could be triggering this automatically?

How many participants join your typical room? I have noticed that when two participants share the same participant_identity,this will cause duplication and trigger the remove participant.

Share some of your logs so that we can take a look & see. Also, take a screenshot of your session events in Livekit Cloud & share that.

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.

Nice reply, and shows great class, with you taking the time to share that info. LiveKit itself is a boss, and I have had no issues with LiveKit for the past 3 months. Below is my docker ps and pm2 status, The CREATED and Status are off, because I brought it all down and back up after making some code changes. And the Created is fresher than the time it has actually been running. I have ran 20 Guest Interviews using LiveKit, and I do have my talking avatar on standby, for when I get the proper server for it. Anytime I thought it might be LiveKit, it was my code that I needed to fix. Great software, and thank you LiveKit for this! Flutter, with Larval is an interesting combo, and I am sure you will figure it out. And again, great reply areeb mohsin, no doubt one of the LiveKit developers. :slight_smile: Below are my docker logs, and my pm2 status, for my App using Livekit

I have developed a Sideline Replay App, and it has been used the past 2 weeks, under the Lights. Pushing it out this week. May go with LiveKit with it, when I push to a Production Server. I could Provide Live Streaming of their Games, along with Side Line Replay, all in one App.

My CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b5e4fd9307 ollama/ollama:latest “/bin/ollama serve” 6 weeks ago Up 4 weeks 127.0.0.1:11434->11434/tcp ollama-service
1e00115ea0 livekit/egress:latest “/entrypoint.sh” 7 weeks ago Up 5 days livekit-egress-1
9d275b314a livekit/ingress:latest “ingress --config /o…” 7 weeks ago Up 5 days livekit-ingress-1
f51f56f7f0bd livekit/livekit-server:latest “/livekit-server --c…” 7 weeks ago Up 5 days livekit-livekit-1

pm2 status – Restarts are all me. It has all been used for the past 3 months

│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 1 │ daphne1-websocket │ fork │ 3 │ online │ 0% │ 118.3mb │
│ 0 │ django1-backend │ fork │ 5 │ online │ 0% │ 25.8mb │
│ 2 │ nextjs1-frontend │ fork │ 10 │ online │ 0% │ 256.3mb │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴────────

@Meharab_Islam_Nibir @DoC_DiD_iT, one field settles this. Read the disconnect reason your Flutter client gets on RoomDisconnectedEvent. The reason tells you whether a real call ended the session or the media timed out, so it decides where you look.

final listener = room.createListener();
listener.on<RoomDisconnectedEvent>((e) {
  // 4 PARTICIPANT_REMOVED or 5 ROOM_DELETED = a real RoomService call ended it.
  // 14 CONNECTION_TIMEOUT or 15 MEDIA_FAILURE = nothing called the API; media died.
  print('disconnect reason: ${e.reason}');
});

The numbers come from the protocol (DisconnectReason).

Your timeline is the real clue. The service worked for 4 months. It broke suddenly. It returns 10 to 12 hours after a clean reinstall. A static config bug does not behave this way, so the cause is not your config. RemoveParticipant and DeleteRoom only run for a token signed with your API secret. So if the reason is 4 or 5 and your code never calls them, another program holds your key. A reinstall does not help if you redeploy the same LIVEKIT_API_KEY and LIVEKIT_API_SECRET, because the caller returns.

One test decides it in a minute. Grep your reverse-proxy access log for the API path: grep 'twirp/livekit.RoomService' access.log. The log line gives the source IP, which names the caller. An IP that is not your Laravel box means the key leaked, so rotate the key and the secret now and the calls stop. Zero external hits means no program called the API, so the reason is 14 or 15, and you check use_external_ip and your open UDP media ports.

Two practical notes for when you run this, since the reason code is easy to misread.

In Flutter, reason is a DisconnectReason enum rather than an integer, so you’ll get names like DisconnectReason.clientInitiated printed out. The Flutter enum also doesn’t line up exactly with the protobuf numbers, so go by the name.

Also worth knowing: there’s an open issue (client-sdk-flutter #626) where RoomDisconnectedEvent fires twice with different reasons. The reporter saw disconnected and then clientInitiated. So log every event with a timestamp rather than just the first one, otherwise you can end up chasing whichever one happened to arrive first.

@Meharab_Islam_Nibir on the results: if the grep comes back empty, that rules out an API call but still leaves duplicate identity, state mismatch and signal close in play, so don’t narrow to media too early. And if it does come back as a removal or a room delete, check your own services first. Egress, ingress and any agent worker all hold the same API key by design, so a call from your own stack looks identical to one from anywhere else.

@DoC_DiD_iT that reason enum is worth logging in your build too, especially once you’re on a production server.