The Virtual Track That Collapsed at 800 Concurrent Attendees: Load-Testing a Conference Platform Before Registration Opens

“800 concurrent attendees” is not a load specification. It is a headcount. A platform can pass 800 in one shape and fail at 300 in another, and the difference is not the number of people — it is the number of rooms, the number of publishers, the join rate, the session duration, and whether the recorder shares a box with the media server. If your pre-registration load test only proves that 800 sessions can be opened, you have tested the lobby, not the event.

This is a protocol for testing the virtual track before registration opens, written for the event operations lead who has to sign the go/no-go. It assumes you already know your platform’s architecture at the level of “where does media terminate and where does presence live.” If you do not, that is the first test.

Why the attendee count is the wrong unit

Conference platforms carry two workloads that fail differently. The first is meeting activity: join handshakes, presence, chat, Q&A, polls, whiteboard, reactions, heartbeats. The second is media: publishing and subscribing to audio and video tracks. They stress different components, and they degrade in different ways.

plugNmeet’s published single-server benchmark makes the split concrete. On a 4-vCore / 16 GB box, meeting activity alone was clean through 500 users in one room with zero missed heartbeats and roughly 100% fan-out. At 1,000 attempted joins, 901 got in, 99 timed out, and heartbeats started missing — the server did not crash, it ran out of headroom. With video on, the same box handled 300 users in a webinar shape (2 cameras, everyone watching) with zero missed heartbeats and video join p95 around 395 ms. The lesson is not “900 is the limit.” The lesson is that the ceiling depends on which workload you are measuring and in what shape. (plugNmeet benchmark)

Meetrix’s Jitsi test shows the same shape-dependence from the other direction. A single Jitsi conference comfortably held around 200–250 participants, with most problems appearing in the frontend rather than the backend. But a single Jitsi shard handled 1,000 participants spread across 100 meetings of roughly 10 each, smoothly. Same total users, different shape, different result. (Meetrix)

LiveKit’s documentation states the constraint plainly: the platform can scale to many simultaneous rooms across multiple nodes, but each room must fit within a single node, which is why its benchmarks focus on concurrent users in a room. (LiveKit benchmark docs) That is an architectural fact you need to know before you write a test plan, because it tells you whether “800 attendees” means one room of 800 or 40 rooms of 20 — and those are not the same test.

Define the traffic shape before you write the script

Write down six numbers for the event you are actually selling tickets to. If you cannot fill them in, the registration page is not the thing that is unprepared.

  1. Rooms and concurrency. How many simultaneous rooms at peak, and how many people in the largest single room? A 900-person keynote and a 900-person workshop with 40 publishers are different systems under test.
  2. Publishers vs. subscribers. How many cameras and microphones are live at once? Viewers are cheap; each publisher multiplies media work. plugNmeet’s own summary is blunt: “Viewers are cheap, publishers cost.”
  3. Join rate. Not just the total, but how fast they arrive. A keynote that fills over 12 minutes is a different stress than a workshop that fills in 90 seconds after a calendar reminder.
  4. Session duration. A 45-minute session and a 4-hour session exercise different leak and reconnect behavior. Soak matters.
  5. Recording. Is the recorder on the same box as the media server? plugNmeet’s benchmarks ran with the recorder idle and explicitly warn that recording plus transcoding on the same box will lower capacity. If your event records, test with recording on.
  6. Registration-open burst. The moment the registration page goes live is a separate load event against the HTTP/API path, not the media path. Test it separately.

An illustrative example, not a case record: a team planning a 900-person keynote with two presenters on camera and everyone else watching is testing a webinar shape. A team planning a 900-person hands-on workshop with 40 publishers is testing a many-publisher shape. The first is closer to plugNmeet’s 300-user webinar row; the second multiplies media work per publisher and should be tested on its own, at its own stop rule. Do not let one test result cover both.

Pick the generator, and size it honestly

You need at least two kinds of load generator, because you have two kinds of load.

For the registration and API path, k6 is a reasonable default. It is an open-source performance testing tool designed to catch performance issues early, and it is explicitly built for high-load tests such as spike, stress, and soak. (Grafana k6 docs) Use it to hit the registration endpoint, the login endpoint, and the schedule/agenda API at the burst rate you expect when the page opens. This is an HTTP workload; RFC 9110 describes HTTP as a stateless application-level protocol, which is a useful reminder that each registration request is independent and the bottleneck will be your database and your rate limits, not a session. (RFC 9110)

For the media and presence path, use the platform’s own load tester if it has one. LiveKit ships an lk load-test subcommand that simulates publishers and subscribers in a room using the Go SDK. Its documentation is specific about the test bench: run it on a machine with plenty of CPU and bandwidth, ensure sufficient file handles (ulimit -n 65535), and run it from multiple machines if needed. (LiveKit) That last point is the one teams skip. The generator is part of the system under test. If the generator box saturates before the server does, the ceiling you observe belongs to your test bench, not the platform.

plugNmeet’s methodology is worth copying as a pattern even if you use a different platform: simulated users speak the same HTTP and WebSocket protocol as the web client, heartbeat every 10 seconds, and exercise chat, whiteboard, notepad, reactions, and hands. The generator was sized at roughly 10 MB RAM per simulated user, and the tester ran in a different city from the server to include real internet latency. (plugNmeet)

Instrument the signals that predict collapse

Server CPU, RAM, and bandwidth are necessary but not sufficient. WebRTC’s bandwidth estimation, bitrate adaptation, and congestion control mean streams will not stay constant across an experiment, so you cannot assume a fixed bitrate per user when you size the box. (webrtcHacks) Measure the client side too.

The W3C WebRTC statistics specification defines the getStats() API and the stat objects you will read: inbound and outbound RTP streams, candidate pairs, transports, and data channels. It notes that packets can be lost and experience varying delay, and that applications are expected to monitor the network and media pipeline. It also recommends computing averages over an interval by calling getStats() twice and taking the difference of sums divided by the difference of counts — not by trusting a single sample. (W3C WebRTC Stats)

Be aware that getStats() itself is not free: the spec states that a call touches many components and may take significant time to execute. If you poll it every 100 ms from 800 clients, you have added a load source. Poll at 1–5 second intervals during the run and accept the coarser resolution.

The signals to record, at minimum:

  • Join success rate: joined vs. requested. A run where 901 of 1,000 joined is not a pass.
  • Join latency p50 and p95. plugNmeet’s webinar rows show video join p50 around 350 ms and p95 around 450 ms on a healthy single server; use your own baseline, not theirs.
  • Missed heartbeats. Zero is the only acceptable number for a clean run.
  • Reconnects. Zero for a clean run.
  • Fan-out delivery. The percentage of chat, whiteboard, and presence messages that reached every intended recipient. Near 100% is the target.
  • Packet loss and jitter from getStats() on a sample of clients, not all of them.
  • Server CPU, RAM, and egress bandwidth sampled at the same interval as the client stats.

plugNmeet’s definition of a clean run is a useful template: joined equals requested, no errors, zero missed heartbeats, zero reconnects, and fan-out near 100%. (plugNmeet) Adopt it or write your own, but write it down before the test.

Set the stop rule before the test, not after

The most common failure in pre-registration load testing is not a technical one. It is that the team runs the test, sees degraded numbers, and negotiates with itself about whether the degradation is “acceptable.” The fix is a pre-committed stop rule.

plugNmeet’s guidance is a good starting point: stop when missed heartbeats rise above zero or fan-out drops below about 100%. That is the server’s honest limit for that shape. (plugNmeet) Your rule can be stricter — for example, stop when join p95 exceeds 1,000 ms or when any single room’s fan-out drops below 99.5% — but it must be written and agreed before the first run, and it must name who has authority to call the stop.

Run a ramp, not a single point. Start at 50 users, double, and stop at the rule. Record the shape at which the rule fired: rooms, publishers, join rate, duration, recording state. That record is the artifact.

Test the registration burst separately from the session-join burst

These are different components and they fail at different times. The registration burst hits your HTTP/API path, your database, and your email or ticketing integration. The session-join burst hits your signaling layer, your TURN servers, and your media servers. A platform that handles 800 session joins can still fall over when 800 people hit the registration page in 90 seconds, and vice versa.

Run the registration test first, because it gates everything else. Use k6 or an equivalent to hit the registration endpoint at the expected burst rate, and watch for HTTP 429s, 5xx responses, and database connection pool exhaustion. Then run the session-join test against a staging environment that mirrors production. Then, if you have time, run them together — because on the day, they will overlap.

If your platform uses TURN relays for attendees behind restrictive NATs, watch TURN utilization during the join burst. plugNmeet’s benchmark notes that its single-server installer co-locates TURN on port 443 TCP and that the logs showed refresh errors from a distant tester; it did not affect those results, but the team recommends separating TURN and media for production. (plugNmeet) If a meaningful share of your audience is behind corporate firewalls, TURN is not an edge case.

Put two real browsers in the room

Bots verify that the server stayed up. They do not verify that the experience was acceptable. webrtcHacks cautions that load tests do not take a deep look at end-to-end user experience, ease of development, or the other functional elements of a successful service. (webrtcHacks) plugNmeet’s methodology addresses this directly: mix bots with at least two real users in normal browsers in the same room during the run, and have them check whiteboard sync, audio and video smoothness, and whether clicks feel instant. (plugNmeet)

Two real browsers is the minimum, not the target. If your event has a moderator workflow — mute/unmute, spotlight, breakout assignment, Q&A moderation — have a real moderator run that workflow during the load test. The moderator path is often the least-tested and most-critical path in a live event, and it is the one that fails when the presence layer is saturated. For more on where the room-to-chat handoff breaks under load, see Why Hybrid Events Fall Apart at the Room-to-Chat Handoff.

What the test does not tell you

A load test is a measurement of a specific shape under specific conditions. It is not a guarantee. The CoSMo comparative study of open-source SFUs notes that published benchmarks differ so much in methodology that direct comparison is impossible. (CoSMo study) That applies to your own tests too: a result from last quarter’s staging environment is not a result for this quarter’s production environment. Re-run before each major event, or at least before each event that changes the shape.

It also does not tell you what happens when a real attendee’s laptop goes to sleep mid-session, or when a speaker’s home upload collapses, or when a sponsor’s tracking pixel fires 800 times in the same second. Those are integration and client-behavior tests, and they belong in the same pre-registration window. If your sponsor integration injects a script into the session page, load-test the session page with that script active.

The artifact: a one-page go/no-go record

The deliverable of a pre-registration load test is not a dashboard. It is a one-page record that a named operational role can sign. The role is the event operations lead, and the record should contain:

  • Shape tested: rooms, largest room, publishers, subscribers, join rate, duration, recording state, TURN configuration.
  • Stop rule: the exact thresholds agreed before the test, and who had authority to call the stop.
  • Observed ceiling: the shape at which the stop rule fired, with the specific metric that fired it.
  • Headroom: the ratio of tested capacity to expected peak, per shape. If you tested 500 in a webinar shape and expect 800, you have negative headroom and the record should say so.
  • Reversible mitigation: the specific action to take if registration overshoots — cap publishers, split the largest room, move the recorder off-box, disable a non-essential integration, or open a second media node. Each mitigation must be testable before doors open and reversible during the event.
  • Re-test date: when the test will be repeated, and what change triggers an earlier re-test.

Keep the record to one page. If it needs two, the shape definition is not tight enough. The point is that a time-pressed operator can read it in 90 seconds and know whether to open registration, delay it, or open it with a mitigation already in place.

FAQ

Can I just ask the vendor for their max concurrent user number? You can ask, but it will not transfer to your event. The same platform shows different ceilings for one big webinar room, many small rooms, and everyone-on-camera. LiveKit’s own docs note that each room must fit within a single node, which is why its benchmarks focus on users in a room rather than total users. Ask for the benchmark shape, not the number.

How long should the load test run? Long enough to catch leaks and reconnect storms, not just the join spike. plugNmeet ran 300 and 500-user tests for 10 minutes each and its 1,000-user push for 18 minutes. If your event is 4 hours, a 10-minute test proves the join, not the session. Run at least one soak at your expected peak shape for the duration of your longest session.

What if the load generator saturates before the server? Then you have measured your test bench, not the platform. Watch generator CPU and memory during the run. If the generator is the bottleneck, distribute it across multiple machines, as LiveKit’s docs suggest, or reduce the per-user polling rate. Do not report a ceiling you did not actually reach.

Do I need to test with recording on? If your event records, yes. plugNmeet’s benchmarks ran with the recorder idle and explicitly warn that recording plus transcoding on the same box will lower capacity. A test with recording off is a test of a different system than the one you will run on the day.

What is the single most useful metric? Missed heartbeats. It is the earliest signal that the presence layer is losing track of attendees, and it fires before the user-visible failures that generate support tickets. Zero is the only acceptable number for a clean run.