Designs by Duhart All writing

·2 min read·audioprocessing · dataengineering · machinelearning · musictech · postmortem

A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio

A post-mortem from my music service: one 118-minute mix took a worker to 9.3 GB, and the decoding rules that came out of it, with the numbers from 3,141 tracks.

Cover slide: A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio. Bottom left, an "Interviewing at" badge with the logos of Spotify, Apple Music, YouTube Music, SoundCloud.

The night a mix took down the demo

On 2026-10-08 at 13:12 UTC my signal engine reached a 118-minute Drive mix and decoded it whole. One worker reached 9.3 GB. The engine shares a 31 GB box with k3s, which leaves it about 8 GB, so the OOM killer took the worker and the portfolio demo pod next to it. k3s restarted the demo within seconds, with no image loss. The engine needed a rule.

The post-mortem in 40 seconds

Numbers on screen come from the engine records and the BillboardML notes; sources at the end.
Opening frame of the video: A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio
Numbers on screen come from the engine records and the BillboardML notes; sources at the end. Watch the video: https://designsbyduhart.org/blog/a-118-minute-mix-ate-9-gb-decoding-audio/

config/config.yml, the signal section (excerpt)

yaml
signal:
  sample_rate: 22050
  n_fft: 2048
  hop_length: 512
  excerpt_policy: "middle"
  excerpt_seconds: 60
  max_decode_minutes: 10       # longer files are decoded as one centred window of this length

The rule: decode at most 10 minutes

A file longer than max_decode_minutes is decoded as one window of that length centred on the middle, by ffmpeg with -ss and -t, so nothing else of the file is ever held in memory. The analysis excerpt (the middle 60 s) lands inside that window, and its offset stays relative to the file. The record says what happened, word for word: long file: 117.8 min; decoded the middle 10.0 min (offset 3234 s); dynamics and loudness measured on that window. The same 7,068.8 s mix now analyses in 27.0 s. 17 tracks in the catalogue take this path.

The fix

Slide 3 of 8 of "A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio", near-black background, white Inter type with mono labels. Never decode more than 10 minutes. A code card: # config.yml, signal: sample_rate: 22050 excerpt_seconds: 60 max_decode_minutes: 10 # longer files: one centred window # the mix: 7,068.8 s long $ ffmpeg -ss 3234.384 -t 600 -i mix.mp3 ... warning: long file: 117.8 min; decoded the middle 10.0 min (offset 3234 s) analysis total: 27.0 s
The setting, the seek, and what the record says.

Two decoders, one output

libsndfile reads mp3 and FLAC natively and is the fast path (2,827 tracks). It cannot read AAC in m4a, which is what every upload is, so those go through ffmpeg as raw float32 (297 tracks). Both produce the same thing: float32 at the rate asked for, plus what the file said about itself. Resampling happens once, at decode, with soxr, so no analyzer ever resamples again. The files arrive at 44,100 Hz (2,149) or 48,000 Hz (992), all stereo; the analysis signal is mono at 22,050 Hz.

What decoding found in 3,141 tracks

Counts from the warnings in every stored record. Hover for what each one means. Open full screen
Cover slide: A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio. Bottom left, an "Interviewing at" badge with the logos of Spotify, Apple Music, YouTube Music, SoundCloud.
Counts from the warnings in every stored record. Hover for what each one means. Interactive version: https://designsbyduhart.org/blog/a-118-minute-mix-ate-9-gb-decoding-audio/

What decoding found

  • 2,364 of 3,141 tracks decode to samples above full scale. The decoder does not clip them, so the engine does not either; it logs the overshoot.
  • 1,356 had a DC offset removed before analysis, because an offset shifts RMS and fools the zero-crossing rate.
  • 5 FLACs failed with read_timestamp() failed in the middle until the decoder learned to retry with an output-side seek.
  • When a worker dies, the whole process pool breaks and cannot say which file did it. The batch rebuilds the pool, retries at full width, then runs the tail one worker at a time, so only the culprit is dead-lettered as WORKER_CRASHED, retryable.

The trade-off

The cap bounds memory, but the loudness of a two-hour mix is now the loudness of its middle 10 minutes. That is a product decision, so it lives in the record's warnings instead of in someone's memory.

Part 2 of 14, next: How Shazam Sees a Song: The Spectrogram Behind My Music AI.


Part 2 of 14, next: How Shazam Sees a Song: The Spectrogram Behind My Music AI.

More: LinkedIn · Instagram. Portfolio and case studies: designsbyduhart.org.

If any of this saved you an afternoon, Buy me a coffee.

A 118-Minute Mix Ate 9.3 GB of RAM: What It Taught Me About Decoding Audio, Designs by Duhart