
Delivery · 15 October 2026 · 8 min
How much headroom to leave for mastering (and why −6 dB isn’t the point)
Leave enough headroom that nothing clips, and send it with no limiter or clipper on the mix bus. Peaks somewhere around −6 to −3 dBFS in a 24-bit or 32-bit float file is a polite convention, not a rule: in 24-bit there’s no audible cost to leaving more, but a clipped fixed-point bounce can’t be undone.
The email says “please leave −6 dB of headroom.” So you pull the master fader until the meter peaks at −6 and export. Or, more often, the mix already sounds great with the limiter on the bus, so you leave it there and just turn the output down to −6. Both feel like you followed the instructions. Only one of them actually did.
The number is the least important part of that request. What the mastering engineer is really asking for is a file they can still make decisions on.
What headroom actually protects
- No clipping baked in. Inside a modern DAW the mix bus runs in floating point, so it can go over 0 dBFS without damage. The moment you export to a fixed-point format like 24-bit WAV, anything over 0 is sliced flat. That damage is permanent.
- No mastering decision made early. A limiter or clipper on the mix bus sets how hard the peaks get pushed before the person whose job that is gets the file. Turning its output down to −6 afterwards leaves the squash in place.
- A little room between samples. The waveform between samples can peak higher than the samples themselves (inter-sample peaks). A few dB of space keeps the file honest on the way into the mastering chain.
What headroom does not do is change the quality of a 24-bit file. The engineer is going to set the gain anyway. A 24-bit file has a theoretical noise floor around −144 dBFS, so leaving 6 dB, 10 dB or even 20 dB of headroom costs nothing you’ll ever hear. The difference between peaking at −3 and peaking at −8 is just a gain knob.
One mix, four exports
We summed a synthetic hot mix (kick, snare, bass, pad, hats) so its float peak landed at +4.5 dBFS, which is common when a mix bus is running hot and nobody’s looking. Then we exported it four ways and ran each file through the check script below. These are synthetic test signals, but the failure modes are the real ones.
| Export | Sample peak | True peak | Integrated | Crest | Clipped runs | Check says |
|---|---|---|---|---|---|---|
| 32-bit float, overs kept | +4.5 dBFS | +4.7 dBTP | −11.7 LUFS | 17.0 dB | — | Recoverable; say so |
| 24-bit, overs clipped | +0.0 dBFS | +2.7 dBTP | −11.7 LUFS | 12.5 dB | 5 | Clipped |
| 24-bit, limiter on the bus (−0.3 ceiling) | −0.3 dBFS | +1.1 dBTP | −12.4 LUFS | 12.6 dB | 0 | Limiter on the bus? |
| 24-bit, turned down 10 dB first | −5.5 dBFS | −5.3 dBTP | −21.7 LUFS | 17.0 dB | 0 | OK to send |
Then we did what a mastering engineer would do first: turn each file down 10 dB and compare it against the clean, unclipped original.
| Export | Error vs the clean original | What that means |
|---|---|---|
| 32-bit float, overs kept | −151.9 dB | Nothing lost. Float only rounds. |
| 24-bit, turned down 10 dB first | −121.2 dB | 24-bit rounding, far below anything audible |
| 24-bit, overs clipped | −33.4 dB | Flat-topped transients, permanently. At the worst moment the error is only 8 dB below the peak itself. |
Three things jump out. The clipped file reads the same −11.7 LUFS as the float one, so a loudness meter won’t warn you. Its crest factor dropped from 17.0 to 12.5 dB: that’s the punch of the kick and snare, gone. And the file that peaked at −5.5 is the cleanest fixed-point export of the lot, even though it’s 10 dB “quieter.”
The limiter file needs its own sentence. It shows zero clipped runs, so a quick clip check passes it. But its crest dropped 4.4 dB, nearly as much as the clipped file, and the peaks keep landing on the same ceiling, about 9% of all 50 ms blocks, against roughly 1% in the untouched exports. That pattern is what the script flags. Also notice that a sample-peak limiter set to −0.3 still let a +1.1 dBTP true peak through. True peak is what codecs clip goes into why.
The send-to-mastering checklist
- Take the limiter, maximizer or clipper off the mix bus. If the mix depends on how it sounds through it, send two files: the clean premaster, and the limited version labeled as a vibe reference. A clipper on the 808 or drum bus is a mix choice and can stay; on the whole mix it’s a mastering choice.
- Peak somewhere around −6 to −3 dBFS. That’s the convention most engineers ask for. Anything under 0 in 24-bit is technically fine; the range just leaves room for inter-sample peaks and stray overs.
- Make headroom at the source, not after a bus processor. Pull the mix bus input trim, or lower the faders or a VCA. Pulling the master fader works in a float DAW only if no plugin after it clips internally.
- Export 24-bit or 32-bit float at the session’s sample rate. No dither unless you’re going to 16-bit, no normalize box ticked, no MP3 preview as the master. Sample rate and bit depth are delivery hygiene has the details.
- If you send float with overs, say so in the email. It’s recoverable, but only if the next person turns it down before anything fixed-point touches it.
- Leave the reverb tail and a breath of room at the end. Don’t trim it tight; the engineer will set fades.
A check you can run before you hit send
This reads any file ffmpeg can open and prints its format, sample peak, true peak, integrated loudness and crest, counts runs of three or more samples stuck at full scale (per channel, so stereo interleaving can’t hide them), and measures how often the peaks land on one ceiling, which is a hint that a limiter or clipper is on the bus. That last one is a heuristic, not a verdict. It needs Python 3, numpy and ffmpeg.
#!/usr/bin/env python3
"""premaster_check.py: is this bounce safe to send to mastering?
Usage: python3 premaster_check.py mix.wav [more.wav ...] (needs ffmpeg + numpy)
Reports format, sample peak, true peak, integrated LUFS, clipped runs and how much
of the file sits pinned near its own ceiling (a hint that a limiter or clipper is on)."""
import sys, re, json, subprocess, numpy as np
def probe(p):
j = json.loads(subprocess.run(["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries",
"stream=codec_name,sample_rate,channels,bits_per_raw_sample,bits_per_sample", "-of", "json", p],
capture_output=True, text=True, check=True).stdout)["streams"][0]
return j
def pcm(p):
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", p, "-f", "f32le", "-"], capture_output=True, check=True).stdout
return np.frombuffer(raw, np.float32)
def loud(p):
err = subprocess.run(["ffmpeg", "-nostats", "-i", p, "-af", "ebur128=peak=true", "-f", "null", "-"],
capture_output=True, text=True).stderr
I = float(re.findall(r"I:\s+(-?[\d.]+) LUFS", err)[-1]); tp = float(re.findall(r"Peak:\s+(-?[\d.]+) dBFS", err)[-1])
return I, tp
def runs(x, thr, minlen=3):
hot = np.abs(x) >= thr
if not hot.any(): return 0
d = np.diff(np.concatenate([[0], hot.astype(np.int8), [0]]))
return int(np.sum((np.where(d == -1)[0] - np.where(d == 1)[0]) >= minlen))
for p in sys.argv[1:]:
s = probe(p); x = pcm(p); pk = np.max(np.abs(x)); I, tp = loud(p)
fmt = s["codec_name"]; floaty = fmt.startswith("pcm_f")
ch = int(s["channels"])
clip_runs = 0 if floaty else sum(runs(x[c::ch], 0.9999) for c in range(ch)) # 3+ samples in a row at full scale, per channel
blk = int(int(s["sample_rate"]) * 0.05) * ch
bp = np.abs(x[:len(x) // blk * blk]).reshape(-1, blk).max(1)
pinned = 100 * np.mean(bp >= pk * 10 ** (-0.5 / 20)) # 50 ms blocks peaking within 0.5 dB of the max
crest = 20 * np.log10(pk / np.sqrt(np.mean(x.astype(np.float64) ** 2)))
notes = []
if floaty and pk > 1: notes.append("overs kept in float: recoverable, but say so when you send it")
if not floaty and clip_runs: notes.append("CLIPPED: flat tops baked into a fixed-point file")
if pinned > 5 and not clip_runs: notes.append("peaks keep landing on one ceiling: limiter or clipper on the mix bus?")
if pk < 10 ** (-24 / 20): notes.append("very quiet: fine in 24-bit/float, just check it's the right file")
print(f"\n{p}\n format {fmt}, {s['sample_rate']} Hz, {s['channels']} ch")
print(f" sample peak {20*np.log10(pk):+.1f} dBFS | true peak {tp:+.1f} dBTP | {I:.1f} LUFS | crest {crest:.1f} dB")
print(f" clipped runs (3+ samples at full scale) {clip_runs} | 50 ms blocks at the ceiling {pinned:.1f}%")
print(" " + ("; ".join(notes) if notes else "OK to send"))
On the four exports above it printed (trimmed to the last line of each):
mix_float.wav overs kept in float: recoverable, but say so when you send it
mix_24_clipped.wav CLIPPED: flat tops baked into a fixed-point file
mix_24_limited.wav peaks keep landing on one ceiling: limiter or clipper on the mix bus?
mix_24_headroom.wav OK to send
If you’d rather not run Python, CoProducer’s analyze page reads clipped samples, true peak and crest for a bounce right in the browser, without uploading it anywhere. Drop the premaster there before it goes out. A zero in the clipped samples row plus a crest that looks like your mix (not like a master) is a good sign.
- Is −6 dB of headroom required for mastering?
- No. It’s a convention. What matters is that nothing clips and there’s no limiter on the mix bus. In a 24-bit file, peaking anywhere from about −10 to −3 dBFS gives the engineer the same quality to work with; they set the gain either way.
- Should I normalize my mix before sending it to mastering?
- No. Normalizing just moves the peak to a fixed level, often right up to 0 dBFS, which removes the safety margin you were asked for. Leave the gain where your headroom puts it.
- Can I send a 32-bit float file that peaks over 0 dBFS?
- Yes, the overs are still in the file and turning it down recovers them exactly. Tell the engineer, though, because any fixed-point step before that turn-down will clip it, and some tools convert on import.
- Should I send my mix with or without the limiter?
- Without, as the main file. If the limiter is part of the sound you’ve been living with, include the limited bounce as a labeled reference so the engineer knows what you liked.