Across 14 clips, Fitbyte landed every file between 96.8% and 100.0% of 10,000,000 bytes. A single-pass encode with the same bitrate budget landed between 80.7% and 100.7%, and on one clip it went over. The quality gap is smaller than I expected. Mean VMAF went from 89.90 to 90.28. Most of the difference is in the worst frames: the 5th-percentile frame VMAF went from 81.26 to 84.65. The cost was 1.05× to 1.49× the encode time.
Fitbyte is the small tool I wrote to squeeze videos under Discord's upload cap. It does two-pass x264 and wraps it in a retry loop. In the Fitbyte post I said the loop "almost always settles in one or two" attempts. I had never checked that, and I had never checked whether the second pass was worth it. So I measured both.

The setup
Clips. Fourteen clips, 15 to 90 seconds each, 592.6 seconds in total, all from freely licensed sources:
- the three Blender open movies (CC BY 3.0)
- three TU München test sequences from the Xiph collection (no restrictions)
- five Wikimedia Commons uploads: beach soccer, street skating, a talking-head interview, a 60 fps screen recording, and a grainy 35 mm trailer scan (CC0, CC BY, CC BY-SA, public domain)
URLs, authors, licenses, and cut points are in the credits at the end. I left out the popular NTIA sports clips because they are licensed for research use only.
Mezzanines. Every clip is cut to a near-lossless mezzanine: x264 CRF 8, FLAC audio. 4K sources are downscaled to 1080p. That one file is both the input to every encoder and the reference for VMAF. So the scores measure loss against the mezzanine, not against the original master.
Box. An 8-vCPU KVM guest on an Intel Xeon (family 6, model 207, AVX-512), with 15 GB of RAM and other things running on the same machine. The software:
- static ffmpeg 7.0.2 (johnvansickle build) with libx264 0.164.3191 and libvmaf 2.3.0
- Fitbyte at commit
6162436
The system ffmpeg is 7.1.5, but Debian's build has no libvmaf, so the static build did all the encoding and all the measuring.
Methods. The bench doesn't re-implement Fitbyte. It imports app.py and calls Fitbyte's own initial_auto_budgets, build_video_encode_command and run_auto_video. The only patch is a wrapper on run_streaming that times each ffmpeg call and saves a copy of each attempt's output. Everything uses -preset slow, AAC at 128k, and +faststart. There were seven methods per clip:
| method | what it is |
|---|---|
crf18 / crf23 / crf28 |
Fitbyte manual mode, single pass, no size target |
abr_1pass |
one pass at Fitbyte's computed -b:v, no retry |
abr_1pass_loop |
Fitbyte's retry loop, line for line, with each two-pass pair replaced by one pass |
twopass_once |
Fitbyte's first attempt only |
fitbyte |
the real run_auto_video |
The budget is plain arithmetic: 98.5% of 10,000,000 bytes, spread over the duration, minus 128 kbps of audio when the clip has audio. That gives 5253 kbps of video for the 15-second clip (no audio track) and 747 kbps for the 90-second one. The two-pass command pair is:
ffmpeg -y -i clip.mkv -map 0:v:0 -c:v libx264 -preset slow -b:v 747k \
-pass 1 -passlogfile X -an -f mp4 /dev/null
ffmpeg -y -i clip.mkv -map 0:v:0 -map 0:a:0 -c:v libx264 -preset slow -b:v 747k \
-pass 2 -passlogfile X -c:a aac -b:a 128k -movflags +faststart out.mp4
The loop counts as done when the file lands between 96.5% and 100% of target. It allows up to five attempts, and from attempt three it accepts any file under the target.
Metrics. libvmaf vmaf_v0.6.1, with PSNR and float SSIM from the same pass. For each encode I kept the mean, the harmonic mean, the 5th-percentile frame and the minimum. Encode time is perf_counter wall time plus child CPU time. I re-timed four clips three times each, with nothing else in the bench running.
I paired the wrong frames first
My first smoke test on a 4-second Sintel cut gave a harmonic-mean VMAF of 2.2. Every third frame scored exactly 0. The filter graph used setpts=PTS-STARTPTS on both inputs. The mezzanines are Matroska with 1 ms timestamps, the outputs are MP4 with a 1/12288 timebase, and at 24 fps the rounding made libvmaf's framesync pair some frames with their neighbours. Switching to frame-index timestamps fixed it:
[0:v]settb=1/1000,setpts=N,format=yuv420p[d];
[1:v]settb=1/1000,setpts=N,format=yuv420p[r];
[d][r]libvmaf=model=version=vmaf_v0.6.1:feature=name=psnr|name=float_ssim:...
The bench now also counts frames in both files. All 98 measured encodes have equal counts. If your VMAF numbers look strangely bimodal, check the pairing before you blame the encoder.
Landing on the number
| method | under 10 MB | under 10 MiB | in 96.5–100% | size, median (min–max) | attempts, mean |
|---|---|---|---|---|---|
| CRF 18 | 1/14 | 1/14 | 0 | 383.2% (64.7–793.6) | 1 |
| CRF 23 | 2/14 | 2/14 | 0 | 168.1% (45.3–428.0) | 1 |
| CRF 28 | 8/14 | 9/14 | 0 | 87.5% (33.9–217.2) | 1 |
| 1-pass ABR | 13/14 | 14/14 | 6 | 96.8% (80.7–100.7) | 1 |
| 1-pass + loop | 14/14 | 14/14 | 12 | 98.7% (92.2–99.8) | 1.86 |
| 2-pass, once | 12/14 | 13/14 | 12 | 99.2% (96.8–107.6) | 1 |
| Fitbyte | 14/14 | 14/14 | 14 | 99.0% (96.8–100.0) | 1.14 |
"10 MB" here means 10,000,000 bytes, which is what Fitbyte targets. 10 MiB is 10,485,760. I didn't test which one Discord actually enforces. Fitbyte aims under both, so it doesn't matter for Fitbyte.
CRF is useless for hitting a size. CRF 23 ranged from 45% of the cap on the screen recording to 428% on beach soccer. Even CRF 28 went over on 6 of 14 clips.
Single-pass ABR mostly undershoots. It left 19% of the budget unused on bbb_action and 18% on screen_tutorial. Then on skate_tokyo it went over, at 10,066,993 bytes. With the retry loop it does land, but it took three attempts on four clips. On two of those, bbb_action (92.8%) and screen_tutorial (92.2%), it hit the "attempt three, good enough" exit without reaching the window.
The surprise was two-pass overshooting. On its first attempt, screen_tutorial came out at 10,764,908 bytes, 7.6% over and above 10 MiB too. tos_dialog came out at 10,070,397. Fitbyte's loop caught both, scaled the bitrate down, and finished on attempt 2. That's 2 of 14 clips that needed a second attempt. So my "one or two" was right, but only because the loop exists. Without it, two-pass alone would have failed the cap on 2 of 14 clips. I don't know why the 60 fps screen recording overshoots by that much. My guess is the pass-1 stats from fastfirstpass on mostly static UI, but I haven't tested that.
What the bytes buy
| method | VMAF mean | VMAF 5th pct frame | PSNR-Y | SSIM |
|---|---|---|---|---|
| 1-pass ABR | 89.31 | 80.32 | 41.61 | 0.9897 |
| 1-pass + loop | 89.90 | 81.26 | 41.90 | 0.9902 |
| 2-pass, once | 90.32 | 84.72 | 41.81 | 0.9909 |
| Fitbyte | 90.28 | 84.65 | 41.74 | 0.9908 |
Averaged over the 14 clips, the second pass is worth about 1 VMAF point against a bare single pass. Some of that is just extra bytes: one pass leaves budget unused, and two passes spend it. The fairer comparison is Fitbyte against the single-pass loop, since both end up close to the same size. There the mean gap is 0.38 points. The 5th-percentile frame gap is 3.39, and PSNR actually goes slightly the other way (41.74 vs 41.90). Two-pass moves bits from easy frames to hard ones, and an average metric barely notices.

The clearest case is tos_dialog. Fitbyte's file was 12 KB smaller than the single-pass file, and its 5th-percentile frame scored 74.56 against 61.53. That's a gain of 13.0 points, on the shots where a viewer would see blocks. The longest clip, sintel_snow_long at 747 kbps, gained 4.05 at the 5th percentile and lifted its worst frame from 53.2 to 65.4.
Two clips go the other way:
teacher_interviewlost 0.56 mean VMAF with two-pass, even though it got 1.0 pp more bytes. Its 5th percentile still went up by 0.96.skate_tokyois a wash: +0.04 mean, +0.03 at p5, with 1.8 pp fewer bytes.
The cap matters more than the pass count. beach_soccer (fast pans over sand and a crowd) gets 1842 kbps for 40 seconds and scores 75.34 with Fitbyte and 74.95 without. CRF 23 on that clip needs 4.3× the budget to score 98.5. No rate-control mode fixes that. Fewer pixels or fewer seconds would.
One thing I can't fully separate: the two methods rarely finish at exactly the same size. For the seven clips where the CRF points bracket Fitbyte's size, I interpolated the CRF curve at that size. Fitbyte came out 0.12 to 0.39 points ahead. So at these bitrates, two-pass ABR is at least as good as CRF at the same size. Interpolating between three points is rough, though.
What it costs

Mean of three runs, wall seconds. Sizes were byte-identical across repeats.
| clip | length | CRF 23 | 1-pass ABR | 2-pass (pass 1 + pass 2) | 2-pass / 1-pass |
|---|---|---|---|---|---|
| tum_pedestrian | 15 s | 9.90 | 10.54 | 13.83 (4.15 + 9.68) | 1.31× |
| tos_dialog | 45 s | 18.46 | 13.49 | 17.74 (6.03 + 11.71) | 1.32× |
| skate_tokyo | 55 s | 23.81 | 20.02 | 21.11 (5.40 + 15.71) | 1.05× |
| screen_tutorial | 60 s | 14.79 | 14.91 | 22.16 (9.28 + 12.88) | 1.49× |
Two-pass doesn't cost double. Pass 1 uses x264's fastfirstpass and skips audio, so it took 26% to 42% of the two-pass total. Pass 2 was sometimes faster than the equivalent single pass: 15.71 s against 20.02 s on skate_tokyo, and 11.71 against 13.49 on tos_dialog. Standard deviations were 0.2–1.4 s.
Over the whole 14-clip bench (one run each, so noisier), the totals:
| method | wall time | vs. 1-pass |
|---|---|---|
| 1-pass ABR | 213.8 s | 1.00× |
| Fitbyte | 355.1 s | 1.66× |
| 1-pass + loop | 401.5 s | 1.88× |
The single-pass loop is the slowest way to hit the target. It needs 1.86 attempts on average, and each miss costs a full encode. Fitbyte's two overshoots cost one extra pair each. The encodes alone took about 38 minutes of wall time in total (that's the sum of measured encode wall time across all 98 encodes; VMAF scoring time wasn't recorded).
What I'm changing, and what I'd use
For Fitbyte:
- Keep two-pass as the default. It doesn't buy much on average, but it reliably helps the worst frames, and that's what people notice in a clip squeezed into 10 MB.
- Keep the retry loop. In the old post I called the five-attempt cap paranoid. The loop earned its place: two-pass overshot on 2 of 14 clips, once by 7.6%.
- Drop the cap from five attempts to three. No clip needed more than two.
- Warn on short budgets. I want to add a warning when video bitrate per pixel per frame drops below a threshold, and suggest downscaling instead. The beach soccer result is what a 1080p clip looks like at 1.8 Mbps, and no amount of rate control will hide it.
For anyone else with a hard size cap and ffmpeg: use two-pass ABR, compute the bitrate from duration minus audio, leave 1.5% for the container, and check the output size afterwards. Don't trust either pass to land exactly. Don't use CRF if you need to hit a size. If you don't have a hard cap, CRF is still the simpler tool.
Clip credits
Fourteen clips, cut from nine sources. All clips were trimmed and re-encoded for the bench. Nothing in this post redistributes the clip files. CC BY and CC BY-SA clips are attributed below.
| Clip | Cut (start s, length s) | Source | Author | License |
|---|---|---|---|---|
| bbb_meadow | 55, 45 | Big Buck Bunny | Blender Foundation | CC BY 3.0 |
| bbb_action | 360, 30 | Big Buck Bunny | Blender Foundation | CC BY 3.0 |
| sintel_snow_long | 12, 90 | Sintel | Blender Foundation | CC BY 3.0 |
| sintel_dragon | 545, 30 | Sintel | Blender Foundation | CC BY 3.0 |
| tos_dialog | 190, 45 | Tears of Steel | Blender Foundation | CC BY 3.0 |
| tos_action | 490, 30 | Tears of Steel | Blender Foundation | CC BY 3.0 |
| tum_rush_hour | whole, 20 | rush_hour_1080p25.y4m | Taurus Media Technik / TU München | No restrictions |
| tum_pedestrian | whole, 15 | pedestrian_area_1080p25.y4m | Taurus Media Technik / TU München | No restrictions |
| tum_tractor | whole, 27.6 | tractor_1080p25.y4m | Taurus Media Technik / TU München | No restrictions |
| beach_soccer | 1, 40 | FIFA Beach Soccer World Cup 2021, match 32 | Voltmetro | CC0 |
| skate_tokyo | 1, 55 | Skateboarding Street World Championship Tokyo 2023 | RuinDig / Yuki Uchida | CC BY 4.0 |
| teacher_interview | 60, 60 | Interview with a Teacher – Glen Caruso | Chase Bohannon, Glen Caruso | CC0 |
| screen_tutorial | 36, 60 | How to create a Wikimedia Commons account | Sarthak Khare | CC BY-SA 4.0 |
| film_grain_1964 | 28, 45 | A Married Woman trailer (1964) | The Scan Man Gallery (scan) | Public domain |
Big Buck Bunny, Sintel, and Tears of Steel are © Blender Foundation, blender.org. The Xiph derf collection lists the three SVT/TUM sequences as no restrictions and no copyright. skate_tokyo and film_grain_1964 were downscaled from 4K to 1080p for the bench. NTIA sports clips on the same derf page are research-use only, so they are not here.
