A single self-hosted Hubs server on a 4 vCPU, 8 GB VPS comfortably carries one room of roughly 15 to 25 participants with audio and avatars, and drops to single digits once most of them switch cameras on. The binding constraint is almost never Reticulum, the Elixir state server, which handles position and object sync at very low cost per user. It is Dialog, the mediasoup based SFU, which must decrypt, route and re encrypt every audio and video stream, and whose outbound bandwidth grows with the square of the participant count. Plan your capacity against Dialog's egress and CPU first, and treat everything else as secondary.
TL;DR by reader profile:
- Two person startup replacing a paid meeting room, for example a design studio doing weekly client walkthroughs: one small VPS is enough, because a 4 person audio first room needs a fraction of a single core and well under 10 Mbit/s of egress.
- Community organiser running open public rooms, for example a monthly meetup with 30 drop in visitors: budget for a dedicated Dialog host and cap video, because unrestricted cameras in a 30 person room push egress into hundreds of Mbit/s.
- Educator running parallel classrooms, for example six seminar rooms of 12 students each: scale by adding Dialog instances rather than by growing one machine, since mediasoup workers are single threaded and bind to one core each.
- Enterprise team behind a corporate firewall, for example a distributed engineering group on locked down laptops: provision TURN capacity explicitly, because relayed peers double the traffic path and can quietly triple your bandwidth bill.
- Hobbyist on a home server, for example a NAS in a cupboard on domestic fibre: check upload speed before anything else, because a typical asymmetric connection saturates its uplink at around 8 to 12 video participants.
- Event host planning a one off launch, for example 100 attendees in a showcase space: shard into multiple rooms or use audio only broadcast patterns, since a single Hubs room is not designed as a webinar stage.
The central tradeoff is simple: every extra participant you allow with video multiplies Dialog's forwarding work and your egress bill, while every restriction you impose on cameras, resolution or room size buys back capacity you can spend on more concurrent rooms.
Table of contents
- What is the realistic concurrent user limit for one self-hosted Hubs server?
- Which Hubs component actually breaks first under load, Reticulum or Dialog?
- How does Dialog's SFU model differ from mesh and MCU, and why does it matter here?
- How much bandwidth does each Hubs participant consume, upstream and downstream?
- Why does room bandwidth grow quadratically, and where does the curve become unaffordable?
- How much CPU does a mediasoup worker use per audio stream and per video stream?
- What server specification do you need for 10, 25 and 50 concurrent participants?
- How much does Reticulum cost per user for position, rotation and object sync?
- When does TURN relay kick in, and what does it add to your bandwidth bill?
- How do simulcast, bitrate caps and audio only rooms change the maths?
What is the realistic concurrent user limit for one self-hosted Hubs server?
Start from the room, not the server. A Hubs room is a single Reticulum channel plus a single Dialog routing context, so the numbers that matter are per room before they are per machine. On a 4 vCPU, 8 GB Linux host running the full Community Edition stack, plan around these ceilings.
| Room style | Realistic ceiling on 4 vCPU, 8 GB | What stops you first |
|---|---|---|
| Audio only, avatars moving | Roughly 25 to 30 participants | Dialog egress, then mediasoup worker CPU |
| Audio plus a few shared cameras | Roughly 15 to 20 participants | Outbound bandwidth on the host uplink |
| Everyone on camera | Single digits, often 6 to 8 | Quadratic forwarding load in Dialog |
| Audio plus screen share at 1080p | Roughly 12 to 15 participants | Sustained high bitrate of the shared track |
| Several parallel small rooms | Limited by core count, not room count | One mediasoup worker binds to one core |
Treat those as planning defaults, not guarantees. Your scene complexity, bitrate caps and network path move them substantially, and the later sections quantify each factor.
Where you run the stack sets the hard limit on the uplink figure. A VPS gives you symmetric bandwidth and a public IP. A home server or NAS gives you cheap storage but usually an asymmetric uplink. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user. Whichever you pick, measure the uplink with iperf3 before you size a room, because Dialog cannot forward what the link will not carry.
Which Hubs component actually breaks first under load, Reticulum or Dialog?
Dialog breaks first, almost every time. The two services fail in completely different ways, and knowing which symptom belongs to which process saves hours of misdirected tuning.
- Reticulum scales the way Elixir services do: it is a Phoenix application on the BEAM virtual machine, and each participant is a lightweight process handling position, rotation and object state over a WebSocket. Thousands of these coexist on one core. You will see memory grow gently and CPU stay flat.
- Dialog scales the way media servers do: it is built on mediasoup, and each worker process is single threaded and pinned to one core. When that core hits 100 percent, every stream it routes degrades at once, regardless of how idle the other cores look.
- The failure signatures are distinct: a saturated Dialog produces choppy audio, frozen video tiles and rising round trip time while avatars keep moving smoothly. A struggling Reticulum produces the opposite, teleporting avatars and stale object positions while voices stay clear.
- PostgreSQL rarely participates: Reticulum writes room metadata, accounts and scene records, not per frame state, so database load stays roughly constant as a room fills.
- The host uplink often beats both: on a 1 Gbit/s link, sustained video forwarding can exhaust bandwidth while both processes still report headroom, which reads as network failure rather than server failure.
Diagnose with top -H -p $(pgrep -f mediasoup) to see per thread CPU on Dialog workers, then compare against the BEAM scheduler load. If one thread sits at 100 percent and the rest idle, you have found your ceiling.
How does Dialog's SFU model differ from mesh and MCU, and why does it matter here?
Dialog is a selective forwarding unit built on mediasoup. It receives one upstream from each participant and forwards copies to everyone else without decoding or re encoding the video. That single design choice explains most of the capacity numbers in this article.
| Architecture | What the server does | Cost at 10 participants with video |
|---|---|---|
| Mesh, peer to peer | Nothing, clients connect directly to each other | Each client uploads 9 copies and downloads 9, clients die first |
| SFU without simulcast, plain forwarding | Copies each incoming stream to every other peer | Each client uploads 1 and downloads 9, server egress is 90 stream copies |
| SFU with simulcast, Dialog's normal mode | Picks a suitable quality layer per receiver | Same stream count, lower total bitrate, no transcoding CPU |
| MCU, server side mixing | Decodes all streams, composites one, re encodes | Each client uploads 1 and downloads 1, server CPU cost is severe |
| Audio only SFU | Forwards Opus tracks, applies no video work | Trivial CPU, bandwidth measured in tens of kbit/s per stream |
The practical consequence is that Dialog trades CPU for bandwidth. It never transcodes, so a mediasoup worker stays cheap in processor terms compared to an MCU like Jitsi Videobridge running in mixing mode. What it cannot escape is the copy count. Ten senders in one room mean 90 outbound stream copies leaving your network interface, and 20 senders mean 380.
That is why you size a Hubs deployment by egress capacity and worker count rather than by raw CPU. It also means adding a faster processor rarely raises your ceiling.
How much bandwidth does each Hubs participant consume, upstream and downstream?
Bandwidth in a Hubs room comes from four separate streams, and only one of them is expensive. Measure each one before you multiply.
- Voice, Opus over SRTP: a single microphone track runs in the 24 to 40 kbit/s range at typical WebRTC settings, plus roughly 10 percent packet overhead. Thirty people talking is still under 1.5 Mbit/s of unique source material.
- Webcam video, VP8 or H.264 simulcast: Dialog normally publishes several spatial layers, commonly around 150 kbit/s at 320x180, 500 kbit/s at 640x360 and 1.2 to 1.5 Mbit/s at 1280x720. Each receiver pulls the layer their connection and tile size justify, so downstream per stream varies widely.
- Screen share at 1080p: treat this as the heaviest single track in the room, typically 2 to 3 Mbit/s sustained for detailed content, and higher during rapid motion. One presenter sharing to 20 viewers costs more egress than 20 voice tracks combined.
- Avatar and object state over Reticulum: position, rotation and interaction messages travel on a WebSocket separate from the media path, measured in single digit kbit/s per user. This is noise against the media figures.
- Scene and asset downloads: a GLB scene, textures and audio files transfer once per join, so a 40 MB scene costs 40 MB per visitor at join time, not continuously.
Upstream per participant is simple: one voice track, optionally one camera, optionally one screen share. Downstream is where the count multiplies. Open chrome://webrtc-internals during a live room and read bytesSent and bytesReceived per transport to confirm your own numbers rather than trusting defaults.
Why does room bandwidth grow quadratically, and where does the curve become unaffordable?
The arithmetic is unforgiving. With n people sending video, each of the other n minus 1 receives a copy, so Dialog emits n times n minus 1 stream copies. Doubling a room quadruples its egress.
- Five senders at 500 kbit/s each: 20 outbound copies, roughly 10 Mbit/s of egress. Any connection handles this.
- Ten senders at the same bitrate: 90 copies, roughly 45 Mbit/s. Still comfortable on a symmetric 100 Mbit/s link, already impossible on most domestic uplinks.
- Twenty senders: 380 copies, roughly 190 Mbit/s sustained. This is where a shared VPS network allowance starts to matter more than CPU.
- Thirty senders: 870 copies, roughly 435 Mbit/s. You are now consuming close to half a gigabit for one room.
- Fifty senders: 2,450 copies, over 1.2 Gbit/s. A single 1 Gbit/s port cannot carry it, whatever the server specification says.
Convert that to metered cost before committing. Sustained 100 Mbit/s of egress transfers about 45 GB per hour, so a weekly two hour session at twenty senders moves several hundred gigabytes per month. Providers that meter egress will notice.
The practical break point sits between 15 and 20 simultaneous video senders on a 1 Gbit/s host. Below it, you tune. Above it, you change the room design.
Where you host decides which side of that line you sit on. A self managed VPS with a symmetric gigabit port, a home server or NAS on asymmetric fibre, and Yundera, a managed Personal Cloud Server built on CasaOS that runs self-hosted apps as Docker containers on a server dedicated to the user, each present a different uplink profile you should confirm before sizing rooms.
How much CPU does a mediasoup worker use per audio stream and per video stream?
Dialog never decodes media, so the CPU cost is packet handling, not pixel work. What a worker actually spends its cycles on is SRTP decryption of the incoming packet, per consumer bookkeeping, then SRTP encryption of each outgoing copy. Cost tracks packets per second, not bitrate alone.
- Audio is cheap because packet rates are low: Opus at the standard 20 ms frame size produces 50 packets per second per stream. Forwarding one voice track to 29 listeners means 1,450 encryptions per second, which a single core absorbs without noticing.
- Video is expensive because packets are many: a 1.2 Mbit/s stream at a 1,200 byte payload runs at roughly 125 packets per second. The same 29 receivers turn that into 3,625 encryptions per second for one sender, and each additional sender adds another block.
- Simulcast multiplies incoming work, not outgoing: the publisher sends several spatial layers, so the worker decrypts all of them but still forwards only the layer each consumer selected. Ingest cost rises modestly, egress cost does not.
- Retransmissions add unpredictable load: NACK and PLI handling from lossy clients forces the worker to resend packets and request keyframes, and a handful of bad connections can cost more than a dozen clean ones.
- Worker count is your only horizontal lever on one box: mediasoup starts one worker process per detected CPU core by default, each single threaded, and a room is pinned to a single worker.
Watch it live with pidstat -u -p $(pgrep -d, -f mediasoup) 1. When one worker crosses roughly 80 percent of a core, stop adding video senders to rooms it owns.
What server specification do you need for 10, 25 and 50 concurrent participants?
Size three things separately: cores for Dialog workers, RAM for Reticulum and PostgreSQL, and uplink for egress. The uplink is usually what you have to buy deliberately.
| Target load | Specification to plan for | The binding constraint |
|---|---|---|
| 4 to 6 people, one test room, audio first | 2 vCPU, 4 GB RAM, 100 Mbit/s symmetric, 40 GB disk | Nothing, this runs the whole Community Edition stack on one host |
| 10 concurrent, mixed audio and occasional video | 4 vCPU, 8 GB RAM, 1 Gbit/s port | RAM, once PostgreSQL, Reticulum and the asset hosts share a box |
| 25 concurrent, audio led with a few cameras | 8 vCPU, 16 GB RAM, 1 Gbit/s port, metered egress checked | Sustained egress, not processor headroom |
| 50 concurrent, video restricted | Dialog on its own 8 vCPU host, Reticulum and PostgreSQL on a second 4 vCPU host | Worker to core pinning, plus port capacity |
| 50 plus, or several parallel rooms | Multiple Dialog instances, one per host, rooms assigned across them | Horizontal scaling, since a room cannot span workers |
Allow 2 GB of RAM for PostgreSQL before tuning shared_buffers, and keep 20 GB of disk free for scene assets and uploads, which grow quietly.
Hosting choices split along the same lines: a self managed VPS gives you the symmetric port and root access to tune kernel networking, a home server or NAS gives you storage but rarely the uplink, and Yundera is a managed Personal Cloud Server built on CasaOS where apps are installed from an app store in one click rather than assembled from compose files by hand. Confirm the egress allowance in writing before you commit to a 25 person room.
How much does Reticulum cost per user for position, rotation and object sync?
Reticulum's cost per user is small enough that it rarely appears in capacity planning, but it is not zero, and it also fans out quadratically. The difference is payload size.
- Per user memory on the BEAM is measured in kilobytes: each participant is an Erlang process holding channel state, so a 30 person room adds megabytes, not gigabytes. Reticulum's baseline footprint dominates, and that baseline is roughly constant.
- Update payloads are tiny: an avatar transform is a handful of floats for position, rotation and a few tracked bones. Even at 10 to 20 updates per second, one sender produces low tens of kilobits per second of unique data.
- The fan out still squares: with 30 people in a room, every update is copied 29 times. Assume 15 updates per second at 50 bytes and the room generates roughly 650 kB/s of broadcast traffic in total, which is under 6 Mbit/s. That is one twentieth of what the same room costs in video.
- Object and media pins hit PostgreSQL, movement does not: spawning, moving and pinning an object writes a row, so a room where people constantly pin content produces steady database writes while ordinary walking around produces none.
- Join and leave events are the spiky part: each join triggers presence updates to every client plus scene metadata queries, so 30 people arriving in the same 60 seconds costs more than the following hour.
Verify on your own instance by attaching to the running node and reading scheduler utilisation with :observer.start() or recon:scheduler_usage/1. If Reticulum is your bottleneck, you have an unusual room, not a typical one.
When does TURN relay kick in, and what does it add to your bandwidth bill?
Most Hubs clients connect straight to Dialog over UDP, because an SFU is already a public endpoint and needs no peer to peer hole punching. Relay only appears when the client side network refuses that path.
- Symmetric NAT and strict firewalls force it: corporate networks that permit outbound 443 only will block Dialog's UDP media range outright, and the ICE negotiation falls back rather than failing.
- ICE over TCP is the cheaper first fallback: mediasoup can accept TCP candidates on the same host, which keeps media on your Dialog server and adds latency and head of line blocking instead of a second machine and a second bandwidth charge.
-
coturn is the real TURN option: it listens on 3478 for UDP and TCP and 5349 for TLS, with a media port range set by
min-portandmax-portin/etc/turnserver.conf. Usestatic-auth-secretwith time limited credentials rather than long lived users. - Relayed traffic is counted twice: every byte enters the TURN server and leaves it again, so a relayed participant pulling 2 Mbit/s costs 4 Mbit/s of measured transfer on that host, on top of the identical traffic Dialog already sent.
- A small relay share is a large bill: in a 20 person video room, moving just 4 participants onto TURN adds their entire downstream a second time, which is a double digit percentage increase on total egress for a fifth of the audience.
Test before you need it with turnutils_uclient -T -u user -w secret your.turn.host. If you skip TURN entirely, accept that some corporate and mobile users will join with avatars visible and no audio at all.
How do simulcast, bitrate caps and audio only rooms change the maths?
These three levers move your ceiling further than any hardware upgrade. Each one attacks the copy count or the bitrate per copy. Take the same 20 person room and compare.
| Configuration | What actually changes | Room egress at 20 participants |
|---|---|---|
| Everyone on camera at 640x360, 500 kbit/s | Nothing restricted, 380 outbound copies | Roughly 190 Mbit/s |
| Bitrate capped to 320x180, 150 kbit/s | Same 380 copies, each one smaller | Roughly 57 Mbit/s |
| Simulcast selecting low layers for distant avatars | Receivers pull the layer their tile size justifies | Between the two figures above, typically nearer the lower |
| Camera rights limited to 4 speakers | Copies fall to 4 senders times 19 receivers, 76 total | Roughly 38 Mbit/s |
| Audio only, avatars and screen share off | 380 Opus copies at about 32 kbit/s | Roughly 12 Mbit/s |
Three practical notes. Simulcast is the default worth keeping, because it costs the publisher extra upstream but saves every receiver downstream, and it degrades gracefully when one participant's connection collapses. Bitrate caps belong in Dialog's mediasoup settings rather than in client code, since anything enforced client side can be bypassed by a browser flag.
Restricting camera rights is the blunt instrument that works. Hubs room settings let you decide who may share video, and moving from open cameras to four presenters cuts egress by 80 percent in the table above without touching resolution at all.
Audio only is not a fallback, it is a valid room design. A 30 person social space with spatial audio, avatars and a good scene stays under 20 Mbit/s and runs on hardware that would collapse instantly with cameras enabled.
Top comments (0)