Back to Blog

Building a Production Audio Streaming Server in Rust

How we built a modern Rust-based audio streaming server to replace Liquidsoap, with multi-mount support, automatic reconnection, and Prometheus monitoring.

Building a Production Audio Streaming Server in Rust

When one of our streaming clients needed a more reliable way to broadcast audio to multiple Icecast servers, we evaluated the usual suspects. Liquidsoap is the go-to solution in this space, but after working with it in production, the pain points were hard to ignore: a complex OCaml-based DSL, cryptic error messages, high memory usage, and no built-in monitoring. Running multiple streams meant spawning multiple processes, each with its own configuration script.

We decided to build something better. The result is a Rust-based streaming server that handles everything Liquidsoap did for us — and quite a bit more — in a single process with a simple TOML config file.

Why Rust for Audio Streaming?

Audio streaming is a domain where performance and reliability intersect. Dropped frames, memory leaks, or unexpected crashes are immediately noticeable to listeners. Rust gave us several advantages:

  • No garbage collector means no unexpected pauses during audio processing
  • Memory safety guarantees prevent the kinds of buffer overflows and use-after-free bugs that plague C/C++ audio software
  • Single binary deployment with no runtime dependencies (aside from the MP3 encoder)
  • Tokio’s async runtime lets us handle multiple concurrent streams, API requests, and monitoring tasks efficiently on a single thread pool

Architecture Overview

The server follows a modular pipeline architecture:

Config (TOML) → Playlist Manager → Audio Pipeline → MP3 Encoder → Icecast Fanout

                    REST API ↔ Dashboard ↔ Monitoring (Prometheus)

Each “mount” is an independent stream with its own playlist, audio processing settings, and Icecast server targets. A single instance can run dozens of mounts simultaneously, each fully isolated.

Audio decoding uses the Symphonia library, a pure-Rust decoder that supports MP3, AAC, FLAC, OGG, and WAV. Audio is decoded in chunks rather than loading entire files into memory, keeping the memory footprint predictable at around 35-50 MB per stream.

Processing includes peak-based normalization (targeting 0.95 amplitude to prevent clipping) and sine-curve crossfading between tracks. Both are configurable per mount.

Encoding shells out to LAME or FFmpeg for MP3 output. While we would have preferred a pure-Rust encoder, LAME’s quality at low bitrates is still unmatched. The server auto-detects which encoder is available at startup.

Multi-Server Fanout

One of the most valuable features is native multi-server fanout. A single mount can stream to two or more Icecast servers simultaneously — useful for primary/backup redundancy or geographic distribution.

[[mounts]]
id = "main"
path = "/stream"
playlist = "/app/playlists/main.m3u"
bitrate = 128
crossfade = 3.0
normalize = true

[[mounts.icecast_servers]]
host = "icecast-primary.example.com"
port = 8000

[[mounts.icecast_servers]]
host = "icecast-backup.example.com"
port = 8000

Each server connection is monitored independently. If one goes down, the others keep streaming while the failed connection retries with exponential backoff (5s → 10s → 20s up to a 5-minute cap). The API exposes per-server status including uptime, bytes sent, and reconnect attempts.

Built-In Monitoring

Every production service needs observability. We built Prometheus metrics directly into the server:

jetstream_cpu_usage_percent
jetstream_memory_used_bytes
jetstream_active_mounts
jetstream_health_status        # 0=healthy, 1=warning, 2=critical, 3=overloaded

Resource limits are configurable, and the server will refuse to create new mounts if CPU or memory thresholds are exceeded. A built-in web dashboard provides real-time visibility without needing to SSH into the server.

The REST API covers everything you would need for automation:

GET    /api/mounts                      # List all streams
POST   /api/mounts                      # Create a new stream
DELETE /api/mounts/:id                  # Stop and remove a stream
POST   /api/mounts/:id/playlist/reload  # Force playlist reload
GET    /api/health/system               # Detailed health check
GET    /api/metrics                     # Prometheus scrape endpoint

Liquidsoap vs. Our Approach

Here is a practical comparison based on our production experience:

AspectLiquidsoapOur Server
Configuration100+ lines of DSL script20 lines of TOML
Multi-streamOne process per streamSingle process, many mounts
MonitoringExternal tools requiredPrometheus + dashboard built-in
ReconnectionManual scriptingAutomatic with exponential backoff
Learning curveWeeksHours
DeploymentOCaml runtime + librariesSingle Rust binary

The biggest win is operational simplicity. Our clients can manage streams through a REST API and monitor health through a dashboard — no terminal access required for day-to-day operations.

Deployment

The server ships as a multi-stage Docker image. The builder stage compiles the Rust binary, and the runtime stage is a minimal Debian image with only the MP3 encoder installed. Total image size is around 200 MB, and it supports both x86_64 and ARM64 architectures.

For systemd deployments, the server runs as a simple service with automatic restart on failure. State persistence means mounts resume automatically after a restart without manual intervention.

Lessons Learned

Rust’s async ecosystem is production-ready. Tokio, Axum, and the broader ecosystem handled everything we threw at them. The combination of async I/O for networking and synchronous processing for audio worked well with Tokio’s multi-threaded runtime.

Shell out when it makes sense. We initially tried to keep everything in pure Rust, but LAME’s MP3 encoding quality justified the external process dependency. Pragmatism beats purity.

Invest in monitoring early. Adding Prometheus metrics and health checks from the start meant we caught resource issues during testing rather than in production. The overhead is negligible — the monitoring task runs every 30 seconds and uses about 0.1% CPU.

Building a custom streaming server was a significant investment, but the operational simplicity and reliability gains have paid for themselves many times over. If you are running audio infrastructure and fighting with Liquidsoap configuration, there is a better way.


KeyQ builds and manages cloud infrastructure and custom software for businesses that demand more than off-the-shelf answers. Get in touch if you are working on something similar.