Skip to content

Capacity: players per server

How many players one bitwerk server can feed, by where the music is stored and how the server is connected. The short answer is the table below; the rest of the page shows where its numbers come from, which of them were measured and which are estimates.

bitwerk sets no limit of its own. Plan with at most 100 players per server and at most 100 rooms per group: more has not been tested on real hardware (the lab has two players), even where the network and the disk would carry more. The control traffic of 500 simulated players was measured (see Measurements).

How many rooms can play at the same time, each a different track, before the storage or the network runs out. Rooms in one group share one read from the disk (see Groups), so for groups only the network rows count. All numbers are estimates with a safety margin (about half of what the hardware should manage), wired Ethernet for server and players, the server on a 1 Gbit/s port unless the row says otherwise.

Music on …CD quality FLAC 16/44.1Hi-res FLAC 24/96Hi-res FLAC 24/192Worst case: DSD256 or WAV 32/384
USB 2.0 stick601582
USB 3 stick> 10055257
External 2.5″ USB hard disk30841
Internal 3.5″ hard disk (NAS)401051
Hard disk with read-ahead raised to 4 MiB (tip)> 10070359
SATA SSD> 100> 1007015
NVMe SSD> 100> 1007015
NVMe SSD, server on 2.5 Gbit/s> 100> 100> 10045
Any storage, server on 100 Mbit/s501071

”> 100” means the hardware would carry more, but 100 players per server is the tested recommendation. From the SATA SSD on, the 1 Gbit/s network is the limit, not the disk: an NVMe SSD adds nothing over a SATA SSD until the server gets a faster port.

A fast NVMe server with “cached” songs: bitwerk keeps no separate cache; every player streams the file from the library. Put the library on the NVMe SSD itself. Linux keeps recently read files in RAM (page cache) on its own, so a group, a track played again or the next track of a popular album often comes from RAM. With the library on SSD or NVMe the storage no longer limits anything; the server’s network port does (rows 6 to 8).

Taken from the code, because it decides what the server has to do per player:

  • No transcoding on the server. GET /api/tracks/{id}/stream of the media-service serves the original file bytes, bit-perfect, with HTTP Range (server/media-service/src/http.rs, tower-http ServeFile). Decoding, resampling, DSD → PCM, ReplayGain and PEQ all run on the player (client/src/engine/). The server’s work per player is reading the file and sending it.
  • One stream per player, paced by its DAC. The player reads the stream into a decoder and a jitter buffer of 2 s (HIFI_BUFFER_MS, playback starts at 500 ms) and stops reading while the buffer is full (client/src/audio/buffer.rs). So a stream runs at the file’s own bitrate on average, with a short burst at each track start or seek while the buffer fills (about 2 s of audio, plus what the TCP buffers hold).
  • Gapless and crossfade: the next track is opened 2 s (one buffer) before the current one ends, for a crossfade the crossfade length before. For that moment one player has two streams open.
  • Groups: every member gets its own Play and streams the file itself (docs/architecture.md, room groups). The control-server only schedules the start 1 s ahead (GROUP_START_LEAD) and relays the leader’s position once a second (SyncReport) to each follower; no audio passes through it.
  • Radio and Spotify do not touch the server at all: each player fetches the radio stream from the internet itself (client/src/radio/), Spotify Connect runs on the player (librespot). They load the internet line (about 0.1 to 0.35 Mbit/s per player, lossless radio about 1 Mbit/s), not the server.
  • Per track start the player asks the media-service for playback-info (one database lookup) and the control-server sends a few small WebSocket messages; idle players ping every 2 s. This is noise next to the audio.

The bitrate of the file is the bitrate on the network. Stereo throughout; plus about 5 % for TCP/IP and HTTP.

FormatUncompressed (WAV, AIFF, DSF)Typical FLACPlanned per player
MP3 320, AAC 256––0.4 Mbit/s
16 bit / 44.1 kHz (CD)1.41 Mbit/s0.6 – 1.0 Mbit/s1.0 (FLAC), 1.5 (WAV)
24 bit / 96 kHz4.61 Mbit/s2.5 – 3.5 Mbit/s3.5 (FLAC), 4.9 (WAV)
24 bit / 192 kHz9.22 Mbit/s4.5 – 6.5 Mbit/s7 (FLAC), 9.7 (WAV)
32 bit / 384 kHz24.58 Mbit/s–26
DSD64 (2.8 MHz)5.64 Mbit/s–6
DSD128 (5.6 MHz)11.29 Mbit/s–12
DSD256 (11.3 MHz)22.58 Mbit/s–24
DSD512 (22.6 MHz)45.16 Mbit/s–48

Uncompressed rates are exact (bits × rate × 2 channels; DSD: 1 bit × rate × 2). FLAC rates are typical values for music; quiet acoustic recordings compress better, loud masters and noise worse. The table above plans FLAC with the upper end of the typical range and the worst case with 26 Mbit/s (WAV 32/384; DSD256 is a little less). DSD512 is half the players of the worst-case column.

Server portUsable for audio (planned)Why
100 Mbit/s50 Mbit/shalf the link
1 Gbit/s500 Mbit/shalf the link
2.5 Gbit/s1,250 Mbit/shalf the link

TCP carries about 94 % of a wired link. The plan uses half, because the same port also carries the bursts at track starts and seeks, the web UI and covers, uploads, the Samba share, backups and whatever else the house sends, and because a switch uplink between server and players may be shared. Every player’s own port is never the limit: even DSD512 needs 48 Mbit/s, which a Pi 3B’s 100 Mbit/s port carries.

Wi-Fi is only a caveat here, since bitwerk’s players are meant to be wired: all players on one access point share its airtime, real throughput is a fraction of the advertised rate (on 2.4 GHz often 20 to 50 Mbit/s for everyone together), and retransmissions come in bursts. The 2 s buffer absorbs short dropouts. As a rough plan, a few CD-quality players per 2.4 GHz access point, hi-res and DSD only wired or on a strong 5 GHz link.

When many rooms play different tracks, the disk serves many files at once: each stream reads its file in order, but the disk sees the streams interleaved, so it jumps between files. Linux reads ahead 128 KiB per jump by default (read_ahead_kb); the media-service reads in 64 KiB chunks. What counts is how many such reads per second the medium manages, not its sequential speed from the box.

MediumEstimated for many interleaved streamsPlanned (≈ half)
USB 2.0 stick15 – 30 MB/s (USB 2.0 tops out near 35 MB/s; no seek time)60 Mbit/s
USB 3 stick50 – 150 MB/s (cheap sticks at the lower end)200 Mbit/s
microSD card (A1/A2, a Pi as server)20 – 40 MB/s80 Mbit/s
2.5″ USB hard disk, 5400 rpmabout 55 seeks/s × 128 KiB ≈ 7 MB/s30 Mbit/s
3.5″ hard disk, 7200 rpmabout 80 seeks/s × 128 KiB ≈ 10 MB/s40 Mbit/s
Hard disk, read-ahead 4 MiBabout 15 – 25 reads/s × 4 MiB ≈ 60 – 110 MB/s250 Mbit/s
SATA SSD300 – 500 MB/smore than the 1 Gbit/s network
NVMe SSD1,000 MB/s and moremore than a 2.5 Gbit/s network

These are estimates from typical device figures, not measurements on bitwerk’s lab hardware; the planned value is about half of the low end. Flash media have no seek time, so with default settings a good USB stick serves more parallel streams than a hard disk, although the disk is faster for one long copy. The disk’s own cache and command queueing help, so real hard disks should do somewhat better than the seek arithmetic.

With 4 MiB read ahead per jump the hard disk spends most of its time reading instead of seeking, which gives it about six times the parallel streams. It costs RAM only while streams run (4 MiB per stream). On the host, for a disk sdb:

Terminal window
echo 4096 | sudo tee /sys/block/sdb/queue/read_ahead_kb
# permanently, e.g. /etc/udev/rules.d/60-readahead.rules:
# ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/read_ahead_kb}="4096"

A group of ten rooms streams ten copies over the network, but the disk reads the file once: the other nine come from the page cache. The storage rows count different tracks playing at the same time; with every room in one group, only the network rows matter. The same holds for a track several rooms play within minutes, as long as the server has RAM to spare (a 24/192 album is about 1 to 2 GB).

The first import of a large library reads every file and measures its loudness (Benchmarks: about 3.3 h for 100,000 tracks on 16 cores). On a hard disk or USB stick that competes with playback: plan with half the players of the table while it runs, or lower SCAN_CONCURRENCY. Later scans only compare size and modification time and cost nothing. There is no limit on the number of indexed files. Import and indexing times by library size and drive: Performance.

The server never decodes or transcodes, so its CPU is not the limit for playback. Measured (see below): the media-service needs about 0.2 to 0.5 of one CPU core per Gbit/s it streams, 300 CD-quality streams took 16 % of one core. A Raspberry Pi 4 or a small NAS CPU is several times slower per core than the machine measured; as an estimate, 500 Mbit/s of streams take one to two of a Pi 4’s four cores there, which still leaves room, but not much for a library scan at the same time.

The database is touched once per track start (playback-info) and by the web UI. Search and library views stay in the tens of milliseconds at 100,000 tracks (Benchmarks), independent of the number of players.

  1. Storage, when many rooms play different tracks from a hard disk or a USB stick: the disk runs out of seeks long before the network is full.
  2. The server’s network port, with SSD or NVMe storage: 1 Gbit/s carries about 70 rooms of 24/192 FLAC.
  3. Wi-Fi, wherever a player is not wired.
  4. The internet line for radio and Spotify, because every player fetches its own stream (the server does not relay it).
  5. Server CPU only on small boards during a library scan; playback itself needs little.
  6. Sync groups add only control messages; the group’s start is scheduled 1 s ahead, and even 500 simulated members received it within 130 ms.

The players have limits of their own (DSD → PCM or resampling on a Pi 3, the DAC), but they do not depend on the number of players.

Measured on 2026-10-20 in a cloud container: 4 vCPU (Intel Xeon, 2.8 GHz), 15 GB RAM, Postgres 16, release builds, everything over loopback.

Media-service streaming (release build of media-service, authentication off, 60 FLAC files of 16/44.1 and 24/192 in the page cache): N parallel HTTP clients (curl --limit-rate) each stream random tracks at a fixed rate, like paced players; the media-service’s own CPU time from /proc/<pid>/stat.

Clients × rateServedmedia-service CPUPer Gbit/s
300 × 1 Mbit/s (CD FLAC)306 Mbit/s16 % of one core0.52 cores
100 × 6.2 Mbit/s (24/192 FLAC)618 Mbit/s19 %0.30 cores
200 × 6.2 Mbit/s1,201 Mbit/s39 %0.32 cores
40 × 26 Mbit/s (DSD256, 32/384)1,051 Mbit/s25 %0.24 cores
8 unthrottled12,710 Mbit/s218 %0.17 cores

Every client got its full rate. Not covered: a real network card (interrupts, offloads), the disk (the files were in RAM) and slower CPUs.

Control-server with many players (server/control-server/tests/scale_bench.rs, real WebSockets, in-memory store; the time until the last simulated player has the message):

PlayersAll rooms Play at onceGroup start (scheduled Play at every member)SyncReport to every follower
505 ms3 ms2 ms
20042 ms33 ms5 ms
500151 ms128 ms10 ms

The group start has 1,000 ms of lead, so a group of 500 still starts together on this machine; on a CPU four times slower it would be about half the lead, which is why groups are recommended up to 100 rooms. Joining 499 rooms one after the other took 1.3 s in total.

To repeat them:

Terminal window
cargo test --release -p control-server --test it scale_bench -- --ignored --nocapture # HIFI_BENCH_PLAYERS=500

The streaming measurement ran a release media-service with AUTH_DISABLED=true and a throwaway database, then N loops of curl -s -o /dev/null --limit-rate 750K http://127.0.0.1:8082/api/tracks/<id>/stream for 30 s.

Not yet measured: storage and network on real hardware. Open item: run the same streaming load on the lab server (bitwerk, wired players) against its own disk, and replace the estimates of the storage table with what it shows.