LiveKit Cloud room with maxParticipants=2 admits 3 STANDARD participants

I’m seeing a reproducible issue where a LiveKit Cloud room reports maxParticipants: 2 but admits 3 simultaneous STANDARD participants.

Environment:

  • LiveKit Cloud

  • livekit-server-sdk@2.15.2

  • Node/TypeScript

  • Room created server-side with RoomServiceClient.createRoom()

  • All participants are normal STANDARD participants, not agents, ingress, egress, or recorders

Reproduction

  1. Create a room server-side:
const room = await roomService.createRoom({
  name: authoritativeRoomName,
  maxParticipants: 2,
});

  1. Verify the returned Room before issuing any participant credentials:
room.name === authoritativeRoomName
room.maxParticipants === 2

  1. Connect two standard participants.

  2. Connect a third distinct standard participant.

Expected

The third participant should be rejected because the room limit is 2.

Actual

All three participants connect successfully.

I also queried the active room directly through RoomServiceClient.listRooms() after all three had connected.

LiveKit returned:

room SID: RM_vy5wh3JsqPbV
maxParticipants: 2
numParticipants: 3

I then queried listParticipants() and confirmed there were three distinct connected participants. All three appear as STANDARD in the LiveKit Cloud dashboard.

The application also validates the Room returned by createRoom() and refuses to issue invite credentials unless the returned room has the expected name and maxParticipants === 2, so this is not just the original CreateRoom request containing the wrong value.

The Room Service API therefore appears to simultaneously report:

maxParticipants = 2
numParticipants = 3

with three distinct STANDARD participants connected.

Is there any circumstance in LiveKit Cloud where STANDARD participants are excluded from maxParticipants enforcement, or could this indicate an admission-enforcement issue?

I can provide additional timestamps, participant identities, screenshots, or reproduction details if useful.

Hi, thanks for the detailed report.

So, what is happening here is the room is being created on the France node, and then you have two additional participants join on the Germany node. You can see in that screenshot the region the users join in.

It takes some finite time for us to sync the room metadata between France and Germany, which includes the max participants. During that time we can either prevent additional users from joining, which would cause delays for the participants, or we could optimistically allow users to join (which is what we do), or we could boot folks out once the maxParticipants sync which would be a confusing and negative experience.

I can see in your logs that you are seeing JOIN_FAILED after the room syncs - it’s just coincidence / timing that just one participant was admitted.

We do have a solution for this for enterprise customers but the workaround that would work for anyone would be to delay a short while after creating the room before allowing participants to join.

Thanks, that explains the behavior and matches what I observed.

Do you have a recommended minimum delay, or a typical/p95 metadata propagation time between European nodes, that you would suggest before issuing participant credentials after createRoom()?

Also, is there any non-Enterprise way to keep room creation and participant admission on the same region, or is the post-creation delay the preferred workaround for Cloud customers?

I’d like to make the workaround deterministic enough that I can verify the two-participant limit before release.

I’m honestly hesitant to give you a number since we don’t publish those metrics externally.

This is just off the top of my head, but if it were me I might add a check to my token server, and only issue tokens to participants if a call to listParticipants returned < the max. That would probably be more robust.

Thanks — that makes sense, and checking listParticipants at the token-issuance boundary seems much more robust than trying to guess a propagation delay.

I’m going to prototype that approach. Little Lantern has only two authorized Storytime roles, host and guest, and each credential deterministically maps to a stable opaque participant identity, so I can make the check identity-aware:

  • if that same identity is already present, allow a reconnect/replacement token;
  • if it is a new identity and fewer than 2 participants are present, allow it;
  • if 2 participants are already present, do not issue another token.

Two questions before I treat this as a hard admission control:

  1. Is listParticipants sufficiently up-to-date/authoritative across Cloud nodes for this purpose, or can its result have the same kind of cross-region propagation delay as the room metadata?
  2. With two simultaneous token requests, both could theoretically call listParticipants, see fewer than 2 participants, and then both receive tokens. Do you normally recommend an application-side reservation/lock around the admission decision, or is that unnecessary with LiveKit Cloud?

One other data point: I tested the delay workaround in a separate LiveKit Cloud test project, and a third STANDARD participant still remained connected even after a 20-second delay before any participants joined. That’s why I’m leaning toward the token-server admission check rather than a fixed delay.

Thanks again — this is very helpful.

Is listParticipants sufficiently up-to-date/authoritative across Cloud nodes for this purpose, or can its result have the same kind of cross-region propagation delay as the room metadata?

listParticipants will be authoritative

With two simultaneous token requests, both could theoretically call listParticipants, see fewer than 2 participants, and then both receive tokens. Do you normally recommend an application-side reservation/lock around the admission decision, or is that unnecessary with LiveKit Cloud?

I think you could manage that in your token server, with some memory to prevent additional participants from joining a specific room once that room has hit its maximum. So, another bullet in your list (if 2 participants have already joined a room, do not allow any more to join)

One other data point: I tested the delay workaround in a separate LiveKit Cloud test project, and a third STANDARD participant still remained connected even after a 20-second delay before any participants joined.

20 seconds? That is very unexpected. I thought we were talking about sub second timescales (maybe a couple seconds at most).