Limitations & capacity
This page is deliberately honest: it summarizes measured capacity numbers and known architectural gaps so you can size a node (or a fleet) without guessing. Figures below were gathered on a production reference node — 8 vCPU / 8 GB RAM, no GPU — on 2026-07-02, using bounded measurements against live production (start one stream + one egress, measure, stop) extrapolated where noted.
The bottleneck: egress RAM
Section titled “The bottleneck: egress RAM”The dominant cost on a node is room-composite egress — LiveKit’s headless-Chrome room compositor used for recording and HLS. Everything else on the media path (the SFU itself, ws-mjpeg camera ingest) is comparatively cheap.
| Component | RAM | CPU | Note |
|---|---|---|---|
| Host idle (no traffic) | ~936 MB used / 6.8 GB free | load ~0.04 | 8 GB RAM + 8 GB swap (0 used) |
egress idle (warm Chrome pool) |
819 MB | 0.7% | container overhead before any recording starts |
ingress idle |
270 MB | 0.05% | |
livekit-server (native) |
~77 MB | ~0.1% | systemd binary, not a container |
core (NestJS) |
~111 MB | ~0.2% | |
| 1 room-composite recording (Chrome) | ~1.27 GiB [measured] | ~0.9 core | 13 Chrome processes, 177 PIDs; +455 MB over the idle container |
| 1 WebRTC publisher + N subscribers | tens of MB in LiveKit | low | the SFU itself is lightweight — the viewer ceiling is the NIC, not RAM/CPU |
| 1 ws-mjpeg camera (ESP32-class) | a few MB [design estimate] | ~0 | last frame in memory + ~50 KB WS connection; the real cost is bandwidth |
Stopping a recording returns the egress container to ~820 MB within seconds.
Concurrent recordings per node
Section titled “Concurrent recordings per node”Budget: 8 GB − ~1.5 GB (system + core + LiveKit + Redis + idle ingress) ≈ 6.5 GB available for egress.
| Egress mode | RAM per recording | Concurrency on 8 GB | When to use it |
|---|---|---|---|
| Room-composite (Chrome) — what StreamHub uses today | ~1.27 GB [measured] | ~4-5 [extrapolated] | Multi-participant composition, custom layout, overlays. |
| Track-composite (ffmpeg, no browser) | ~250-400 MB [extrapolated from LiveKit docs] | ~15-20 | The common case (1 publisher → 1 MP4) — not implemented yet, tracked as a roadmap optimization. |
| Track egress (ffmpeg, single track) | ~150-250 MB [extrapolated] | ~25+ | A single raw track. Also not implemented yet. |
Switching the common single-publisher case from room-composite (Chrome) to track-composite
(ffmpeg) is the single biggest lever for more recordings per node — roughly 5x the
concurrency — but it requires code changes to recording.service that haven’t landed yet.
WebRTC viewers
Section titled “WebRTC viewers”Live interactive delivery is SFU-forwarded, so the ceiling per node is the network interface, not CPU or RAM: roughly 300-400 concurrent viewers on a 1 Gbps NIC at ~2.5 Mbps/viewer. For larger one-way audiences, route to HLS + CDN/P2P instead of adding more WebRTC nodes — a single 8 GB origin behind a CDN pull zone can front an event in the 10k-100k viewer range.
ws-mjpeg cameras (CCTV-style ingest)
Section titled “ws-mjpeg cameras (CCTV-style ingest)”RAM cost per camera is negligible — the limiting resource is bandwidth, not RAM/CPU. At QVGA/8fps (~0.5 Mbps/camera), a 1 Gbps node can carry on the order of 600+ cameras before saturating the NIC; VGA/15fps (~2-3 Mbps/camera) saturates much sooner. For large camera fleets, prefer QVGA and spread across multiple nodes.
Offloading to a GPU node
Section titled “Offloading to a GPU node”A node with an NVIDIA GPU (joined via install.sh --join, sharing the origin’s Redis) changes
the picture for CPU-heavy work:
| Workload | On 8 vCPU / 8 GB, no GPU | On an NVIDIA GPU node | Gain |
|---|---|---|---|
| Transcode ladder (RTMP ingest, adaptive VOD) | CPU x264 — expensive | NVENC — many parallel encodes | Frees origin CPU; the ladder becomes effectively free |
| Egress / recording | ~1.27 GB Chrome each | GPU-accelerated + more RAM headroom | Many more concurrent recordings |
The yolo plugin (object detection) |
CPU — slow | CUDA | Real-time, multi-camera |
The per-app hwaccel setting (auto/gpu/cpu) and GET /system/gpu detection already
exist (see Per-app config.yaml); routing that work onto a
GPU-equipped cluster node today is a manual placement decision, not automatic — there is no
autoscaling that shifts work to GPU capacity on demand.
Suggested default quotas
Section titled “Suggested default quotas”The built-in free-plan defaults (enforced per tenant when STREAMHUB_AUTHZ_ENFORCE=on,
reported via GET /tenants/:id/usage):
| Metric | Free-plan default | Enforced before |
|---|---|---|
maxApps |
2 | POST /apps |
maxConcurrentStreams |
2 | minting a publisher token, POST /apps/:app/ingress |
maxRecordingMinutesMonth |
300 | POST .../recording/start, .../record/start |
maxEgressGbMonth |
5 | POST /apps/:app/broadcast/start |
maxStorageGb |
5 | reported; storage accounting |
For a shared multi-tenant node (8 vCPU / 8 GB), a reasonable per-app operating budget —
distinct from the free-plan defaults above — is max_concurrent_streams ≈ 10 and
max_concurrent_recordings ≈ 3 with today’s room-composite egress (or ≈ 10 once
track-egress ships).
Known architectural limits
Section titled “Known architectural limits”- Room-composite egress is the only recording/HLS pipeline today. Track/track-composite egress (5x cheaper for the common single-publisher case) is a documented, unimplemented optimization.
- No LL-HLS/CMAF, no DASH. LiveKit’s egress produces standard HLS-TS segments, typically 6-15 seconds of glass-to-glass latency. Low latency in StreamHub is the WebRTC path (sub-second), not HLS. LL-HLS/CMAF is on the roadmap as a future paid/EE module.
- No SRT ingest. LiveKit itself has no native SRT ingestion; only RTMP, WHIP and RTSP are supported ingest paths.
- No DRM / HLS encryption. Playback security today is unsigned public HLS/WebRTC; there is no AES-encrypted HLS or DRM integration.
- No autoscaling. Neither the egress Chrome pool nor transcode capacity scale up/down automatically with load — capacity is whatever the node (or a manually-joined GPU node) provides.
- Single shared LiveKit API key/secret across a cluster.
POST /cluster/joinhands the same LiveKit key/secret to every edge node. There is no per-node or per-tenant key isolation yet — treat the cluster token and LiveKit credentials as a single trust boundary. - No cross-node media routing — the cluster is a node registry, not a router. Each LiveKit room is served by exactly one node (chosen by LiveKit via the shared Redis); StreamHub’s cluster module provides join/heartbeat/drain but no capacity- or region-aware room placement. A real known consequence: a recording’s egress can land on a different node than the room it’s recording, writing the MP4 to a disk the origin core never looks at — the recording then fails silently. Avoid mixing recording workloads across nodes that share a LiveKit/Redis mesh until this is addressed.
- No restream to multiple destinations. Broadcast pushes to a single
rtmpUrl; fanning out to several RTMP/SRT endpoints at once isn’t implemented. - No built-in TURN server. Networks that block direct/STUN-negotiated UDP (an estimated 5-15% of restrictive corporate/mobile networks) may fail to connect; there’s no bundled coturn in the installer today.
See streamhub-docs/PRODUCT-EVALUATION.md and streamhub-docs/operations/CAPACITY-G2.md in
the repo for the full audit these numbers are drawn from.