qumo is a high-performance Media over QUIC (MoQ) relay server with peer-based content discovery, enabling distributed media streaming over the QUIC transport protocol.
📚 Documentation: https://qumo-dev.github.io/qumo/
- 🚀 High-Performance Relay: Built on QUIC for low-latency media streaming
- 📡 MoQT Protocol: Full Media over QUIC Transport support (moq-lite draft-04)
- 🔗 Peer-Based Topology: Relays connect to each other via ANNOUNCE_PLEASE for decentralized content discovery
- 📊 Observability: Prometheus metrics, health probes, and status APIs
- 🔒 TLS Security: Built-in TLS 1.3 support for encrypted connections
- 🐳 Docker-Support: Env-var zero-config; prebuilt multi-arch images on GHCR (ghcr.io/qumo-dev/qumo)
Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/qumo-dev/qumo/main/install.ps1 | iex"Linux & macOS:
curl -fsSL https://raw.githubusercontent.com/qumo-dev/qumo/main/install.sh | sh(You can also use irm https://qumo-dev.github.io/qumo/install.ps1 | iex or curl -fsSL https://qumo-dev.github.io/qumo/install.sh | sh)
go install github.com/qumo-dev/qumo@latestDownload the latest archive from GitHub Releases:
# Linux/macOS
curl -L https://github.com/qumo-dev/qumo/releases/latest/download/qumo_0.6.260906_linux_amd64.tar.gz | tar xz
./qumo playground # one-command demo: relay + web UI at http://127.0.0.1:8080
# Or for a standalone relay:
mage cert # generate a dev cert (mkcert or self-signed)
./qumo relay # start the relay (certs/server.crt + .key)
# Windows: download qumo_0.6.260906_windows_amd64.zip from the releases pageSee docker/README.md for compose examples, GHCR usage, and deployment options.
git clone https://github.com/qumo-dev/qumo.git
cd qumo
mage build # builds bin/qumo with version info
# or: go build -o qumo .qumo relay # Start MoQ relay server (QUIC/MoQT, WebTransport, peer mesh)
qumo rtmp # Start RTMP ingest server (bridges RTMP → MoQT)
qumo rtsp # Pull from an RTSP source (e.g. IP camera) and republish as MoQT
qumo rtsp-push # Start the RTSP push ingest server (bridges RTSP → MoQT)
qumo hls # Start the HLS/DASH egress server (subscribes to a MoQ relay, serves HLS/DASH)
qumo funnel # Funnel many HTTP senders into one MoQT track (records each POSTed record, then serves it)
qumo playground # One-command local demo: in-process relay + embedded web UI on http://127.0.0.1:8080
qumo loadgen # Out-of-process capacity load generator (publish|subscribe) — see Benchmarking
qumo update # Update qumo to the latest release (--check to verify without applying)
qumo version # Print build-time version infoFor environment variables and configuration, see relay-config.example.env. For Docker-based deployment, see docker/README.md.
qumo's servers (relay, rtmp, rtsp, rtsp-push, playground) present TLS from CERT_FILE / KEY_FILE (default certs/server.crt + .key; generate one with mage cert). The clients that dial a relay verify its certificate by default — verification is the default, not an opt-in:
-
qumo loadgenandtools/capacityrequire the relay's cert as a trust anchor (--ca <cert.pem>), or--insecurefor a self-signed dev relay. Verification is the default;--insecureis the explicit escape hatch. -
The bench/test clients follow the same convention:
bench-multiproc/tools/latency-probetakes--caor--insecure;internal/smoketest(themage smokeharness) takes-caor-insecure, withmage smokepassing-insecureagainst the self-signed Docker topology. -
qumo hlsverifies against the system root store by default. Trust a specific relay cert withRELAY_CA_FILE, or disable verification for a self-signed dev relay withRELAY_TLS_INSECURE=true:qumo hls # verify against the system roots RELAY_CA_FILE=certs/server.crt qumo hls # trust this relay's cert RELAY_TLS_INSECURE=true qumo hls # dev relay with a self-signed cert
cmd/seed-moq, the dev seeder, presents an ephemeral self-signed certificate — point the egress at it with RELAY_TLS_INSECURE=true.
Relays authenticate each other with certificates a CA of yours issued (CA_FILE, PEER_CERT_FILE, PEER_KEY_FILE), separate from the public certificate they serve browsers. A relay with PEERS refuses to start without them. mage cert writes development ones; see Deployment → TLS & mTLS for what a peer certificate must contain.
graph LR
Publisher["Publisher<br/>(Browser/WebTransport)"]
Hub["Hub Relay<br/>(qumo relay)"]
EdgeA["Edge Relay A<br/>(qumo relay)"]
EdgeB["Edge Relay B<br/>(qumo relay)"]
Subscriber["Subscriber<br/>(Browser/WebTransport)"]
Publisher -->|"QUIC/MoQ<br/>WebTransport"| EdgeA
EdgeA <-->|"ANNOUNCE_PLEASE<br/>QUIC peer"| Hub
Hub <-->|"ANNOUNCE_PLEASE<br/>QUIC peer"| EdgeB
EdgeB -->|"QUIC/MoQ<br/>WebTransport"| Subscriber
On startup, each relay dials the static, comma-separated addresses in PEERS (e.g. an edge relay's hub, or any relay hierarchy) — there is no runtime service discovery. Each host is resolved to all its addresses (e.g. role-hub.qumo-relay.service.consul:4433 for a group of hubs), and every address is dialed once at startup and re-dialed with backoff on disconnect. --role <hub|edge> is an operator-facing label logged for visibility only; it does not affect which peers are dialed.
Each connection dials QUIC with ALPN moqt, exchanges ANNOUNCE_PLEASE / ANNOUNCE, and registers the peer's tracks on the local TrackMux. On disconnect the connection is retried with exponential backoff (1s–30s).
graph TD
Start["Relay Startup"]
Start -->|"for each PEERS address"| ALPN
ALPN["QUIC dial (ALPN: moqt)"] --> Announce["ANNOUNCE_PLEASE / ANNOUNCE"]
Announce --> TrackMux["Register tracks on local TrackMux"]
TrackMux --> Serve["Serve subscribers"]
ALPN -->|"failed"| Retry["Backoff (1s-30s) → retry"]
Serve -->|"disconnected"| Retry
Retry --> ALPN
- Go 1.27+ — Download or use your package manager
- Deno (required for
mage build; the Go binary embeds the web UI built by Deno + Vite) — Download - Mage — Build automation tool
Then run
go install github.com/magefile/mage@latest
mage helpto see all available tasks.
For complete Mage documentation and all available targets, see magefiles/README.md.
qumo/
├── docker/ # Docker artifacts & docs
│ ├── Dockerfile # Multi-stage container build
│ ├── docker-compose.yml # Single relay (local build)
│ ├── docker-compose.external.yml # Single relay (GHCR prebuilt)
│ ├── docker-compose.static.yml # 3-region topology, static PEERS (no discovery)
│ ├── docker-compose.nomad.yml # Single-region Nomad cluster (PEERS sim)
│ ├── nomad/ # Nomad agent config + job spec
│ └── README.md # Docker usage guide
│
├── internal/ # Core implementation
│ ├── relay/ # Relay server (handlers, peer resolvers, caching, session auth)
│ ├── ingest/ # RTMP & RTSP ingest (push + pull), codec init-data builders
│ ├── rtmp/ # RTMP protocol stack
│ ├── rtsp/ # RTSP protocol stack & RTP de-packetization
│ ├── playground/ # One-command demo server (relay + embedded web UI + /api/pull)
│ ├── loadgen/ # Out-of-process capacity load generator (qumo loadgen)
│ ├── cors/ # WebTransport origin validation (CSWT mitigation)
│ ├── smoketest/ # Cross-region streaming smoke test harness
│ └── version/ # Version info
│
├── magefiles/ # Build automation (Mage tasks)
│
├── scripts/ # Bench dashboard generator (Deno)
├── tools/capacity/ # Capacity driver: sweeps/ceiling-finds by driving `qumo loadgen`
├── tools/paramexp/ # Parameter-space explorer (GP surrogate; separate module)
├── docs/ # Design docs
├── playground/ # Web demo / relay test client (Deno + Solid)
├── .github/workflows/ # CI/CD pipelines
├── go.mod & go.sum # Go dependencies
└── main.go # Entry point (CLI dispatch)
See magefiles/README.md for the complete reference. Common targets:
mage build # Build binary to bin/qumo
mage test # Run tests
mage check # Format, vet, and test
mage lint # Run golangci-lint
mage docker:build # Build Docker image
mage relay # Run relay server locally
mage smoke # Run cross-region streaming smoke testThe relay's benchmarks emit JSONL that a zero-dependency Deno tool turns into a single, self-contained dashboard — open one file, no server:
# After a bench run has written results.jsonl into <dir> (BENCH_RESULTS_DIR):
deno run --allow-read=<dir> --allow-write=<dir> scripts/relay_bench_report.ts <dir>
# → <dir>/index.html : capacity headline + decision summary + every plot inline,
# plus the paramexp GP/ML findings when passed --paramexp <report-dir>.Concurrent-session capacity is measured out-of-process. Running the relay
and the load clients in one process makes client-side QUIC-handshake CPU — not
the relay — the bottleneck, so the load runs against a separately running
relay and the measurement reports the relay's own per-session cost by
scraping its /metrics (go_goroutines, process_resident_memory_bytes,
qumo_relay_sessions_active) before/after the run.
The qumo loadgen CLI is two small primitives — pure remote clients that dial
the relay you point them at and never spawn one:
qumo loadgen publish --relay <host:4433> --ca <cert.pem> # trickle source
qumo loadgen subscribe --relay <host:4433> --ca <cert.pem> --hold 15s 12000 # measure N=12000subscribe --results <dir> appends a capacity-group record to
results.jsonl, which the dashboard renders.
Sweeping / finding the ceiling is orchestration, so it lives in a separate
driver — tools/capacity — that composes the primitives (starts a relay +
publisher, then probes session counts). Build it with go build -o capacity ./tools/capacity. It runs two ways:
-
One box:
--start-relayspawns a local relay (self-signed cert generated in-process — noopenssl) pinned via--relay-coresso its CPU is isolated from the load — a single-box stand-in for two hosts. A fresh relay is started per probe, so each measurement is independent and never inherits residual state from a prior overload.--sessionsprobes an explicit list;--autoclimbs until the relay can't hold to find the ceiling (--bisectpins the boundary). This is what thecapacity-sweepjob in.github/workflows/bench-relay.ymlruns:./capacity --start-relay --relay-cores 0-1 --sessions "500 1000 2000" --hold 10s ./capacity --start-relay --relay-cores 0-1 --auto --start 2000 --max 50000 --bisect -
Two hosts: point it at a relay running elsewhere; it only generates load:
./capacity --relay relay.example.net:4433 --ca cert.pem --auto --start 5000 --max 30000
Every probe appends to results.jsonl, so a run renders in the same dashboard.
A distributed (multi-machine) run is what a 25K-session ceiling claim needs to
be confirmed rather than extrapolated.