Skip to content

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 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.

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.

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.

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.

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.

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).

  • 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/join hands 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.