A 200-viewer admission limit on top of MistServer
The Selectel VM has a 1 Gbit port. At 3 Mbps per stream, roughly 300 viewers consume the whole thing — including the headroom needed for ingest, SSH and routine server work.
Ahead of a live international tournament, I set admission at 200 viewers, about 600 Mbps. My rule here is simple: reject viewer 201 cleanly rather than let a saturated port ruin playback for all 300.
MistServer has no such limit
MistServer OSS 3.x has no native global cap for viewers or outbound bandwidth. I checked the documentation; claims that it does exist are ungrounded.
Two nearby controls do different jobs:
→ LIVE_BANDWIDTH is a blunt kill switch. A false response kills the entire stream buffer, not one incoming session. That is unusable on a live service
→ RTMP maxkbps applies to ingest only. It never sees viewer delivery
The decision therefore has to happen when a new session asks to enter.
USER_NEW is the admission point
The USER_NEW trigger calls a small synchronous Python handler. It runs as a systemd service and listens on loopback only, port 9901.
For every connection, the handler:
→ reads the current viewer count from active_streams
→ bypasses the limit for ingest connectors, so a viewer rule cannot cut off the source signal
→ admits the session while the count is below 200
→ writes every decision to a JSON log: stream, connector type, address, count and rejection reason
That log doubles as rejection analytics. It shows how many people arrived after the port filled without another measurement system.
The handler is deliberately fail-open. If port 9901 is unavailable or the count cannot be read, MistServer admits the viewer. A fault in my wrapper must not take down a healthy stream; the hypervisor will provide the hard bandwidth boundary later.
My first registration was wrong: the new synchronous handler replaced an existing asynchronous telemetry handler. A configuration check caught it, and I registered admission as a second handler alongside telemetry. One trigger can serve both, provided both destinations are explicitly retained.
From synthetic checks to a real rejection
The full matrix passed:
→ normal viewer allowed
→ synthetic viewer denied at cap=0
→ ingest bypassed at cap=0
→ trigger fired on a fresh stream with a real IP
→ with cap=1, one held session occupied the only slot and a third-party cloud IP was denied
The last check mattered most. The rejected viewer did not hang on an empty request: MistServer returned a clean playlist containing #EXT-X-ERROR: Shutting down due to session end. The client got a final answer and the port stayed in its operating range.
Old stream processes keep the old trigger state
One operational gotcha: stream processes that predate handler registration continue with their existing configuration. Admission starts applying to them only after a stream restart.
The rollout order is therefore: register both handlers, inspect the handler list, restart the stream, then repeat the external check. Saving the setting alone is insufficient.
The next change fits the same handler. MistServer already issues ?tkn= per session, so an HMAC signed-URL check can ride this path. A Proxmox net0 rate 700 Mbit limit will become the hard backstop after the tournament — no NIC replug on the night before a live event.
More as it arrives.