FFmpeg CRF vs bitrate, and why ABR overshoots

Compare CRF, target bitrate and VBV-capped modes in FFmpeg on one clip. Single-pass 700k ABR overshot by 13%, and adding a VBV cap landed within 0.2% of target.

Share

Single-pass -b:v 700k overshot its own target by about 13% on a 5-second clip. The same target with a VBV cap (-maxrate and -bufsize) landed within 0.2% of it, so the cap, not the target, is what made the bitrate predictable. For streaming I would use CRF with that cap, and for a hard size limit a two-pass or target-size encode. The shared clip and setup are on how we benchmark.

Terminal window
# the four modes, libx264 preset medium, audio stripped
ffmpeg -i sample.mp4 -c:v libx264 -preset medium -crf 23 -an -t 5 out.mp4
ffmpeg -i sample.mp4 -c:v libx264 -preset medium -b:v 700k -an -t 5 out.mp4
ffmpeg -i sample.mp4 -c:v libx264 -preset medium -b:v 700k -maxrate 900k -bufsize 1400k -an -t 5 out.mp4
ffmpeg -i sample.mp4 -c:v libx264 -preset medium -crf 23 -maxrate 900k -bufsize 1400k -an -t 5 out.mp4
CRF against target bitrate
Should you ask for a quality or ask for a bitrate?
Held constant: libx264 preset medium, same source, audio stripped
Bar chart. CRF against target bitrate. Encode time and output size for each variant, each metric scaled to its own maximum.
VariantEncodeSizeCost
CRF 23 (quality)1596 ms433.0 KB$0.0031
700k ABR948 ms fastest483.0 KB$0.0023
700k capped VBV1074 ms428.1 KB$0.0025
CRF 23 capped VBV1602 ms417.0 KB$0.0030
Measured 2026-08-20 on the Rendobar API. Encode time is the FFmpeg step alone, separated from download and upload. 4 of 4 runs succeeded. Sizes are exact and repeatable. Timings are a single sample and vary up to 2x run to run, so treat small differences as noise. Why.

Plain ABR missed its own target

700 kbps for 5 seconds is 437,500 bytes, which is 427.2 KB. The plain ABR encode wrote 494,611 bytes (483.0 KB), about 13% over the target. An earlier version of this post said 10.4%, because it set 437.5 decimal kB against the binary KB in the table. The file was the same, the arithmetic was wrong.

This is normal behaviour. Single-pass -b:v aims at a target without knowing what is coming, so it corrects as it goes and lands near the target, not on it.

The practical point matches the one in Opus vs AAC vs MP3 vs FLAC: if you size storage from bitrate times duration, add margin.

The cap is what hit the target

Adding -maxrate 900k -bufsize 1400k to the same 700k target produced 438,402 bytes, 0.2% over the arithmetic. Plain ABR did not come close. On this clip the VBV constraint, not the average target, is what held the bitrate where it was asked to be.

This is the most useful row in the sweep, and the one I would design around.

Capped CRF gives quality with a ceiling

Terminal window
-crf 23 -maxrate 900k -bufsize 1400k

This encodes to a constant quality of 23 but never lets the bitrate exceed 900 kbps over a 1,400 kbit buffer. Quality drives the encode, and the cap only steps in on the hardest passages.

Plain CRF gives consistent quality with unbounded peaks, which can stall a player on a complex scene. Plain ABR gives a predictable average with no quality floor, so simple scenes waste bits and hard scenes fall apart. Capped CRF gives quality with a ceiling, and that bounded peak is the reason to use it.

Capped CRF also wrote a smaller file than plain CRF. That is not a win on its own. A file shrinks under a cap because the cap lowered quality on the passages where it bound, and I have no VMAF score for these four outputs to say how much. Read the size column as what each mode delivers, not as a quality ranking.

What -bufsize does

-maxrate is the ceiling. -bufsize is the buffer the ceiling is measured over, and it is the flag people omit or copy without understanding.

A large buffer lets the encoder average over a long stretch, spending heavily on a hard scene and recovering later. A small one keeps it near the cap moment to moment, which is stricter and costs more quality.

-maxrate without -bufsize is ignored outright in libx264. With no buffer size there is no VBV window to measure against, and x264 logs a warning saying the maxrate was ignored. It is one of the most common half-configured flags in FFmpeg.

A 5-second clip barely exercises the buffer model. VBV behaviour matters most over minutes, with real scene changes to budget across, so treat the capped result as a clean pass on an easy case.

Which I would use

For download or archive, plain CRF. Peaks do not matter when nothing streams, and quality stays consistent.

For streaming, an ABR ladder or anything with a player buffer, capped CRF.

For a hard file-size limit, two-pass ABR. I did not run two-pass in this sweep, but it is the standard fix for the overshoot above:

Terminal window
ffmpeg -y -i in.mp4 -c:v libx264 -preset medium -b:v 700k -pass 1 -an -f null /dev/null
ffmpeg -i in.mp4 -c:v libx264 -preset medium -b:v 700k -pass 2 -an out.mp4

The alternative is to let a job search for the size, which is what compress video to a target size covers.

Running the four modes through the API made them cheap to compare: each mode was one job on the same source, and the output size came back exact with each result.

Rate control is one of the settings in FFmpeg encoding settings compared. I would drop plain single-pass ABR entirely. It is the one mode here that guarantees neither the quality nor, as measured, the bitrate.

Frequently asked questions

What -bufsize should I use with -maxrate?

One to two times -maxrate is the usual starting point. I used 1400k with a 900k cap. A smaller buffer holds the bitrate closer to the cap moment to moment, which is safer for live players and costs quality on hard scenes.

Does two-pass encoding work with CRF?

No. Two-pass is a bitrate mode: the first pass analyses the whole file so the second can spend a fixed budget well. CRF has no budget to plan, so it runs in one pass.

Sources

Tags #ffmpeg#x264#streaming#encoding#benchmarks
All posts
Share
  1. How we benchmark FFmpeg Engineering blog
  2. Custom fonts in a video API fail silently Engineering blog
  3. Opus vs AAC vs MP3, requested vs delivered bitrate Engineering blog