Rate control is the part of an FFmpeg command that decides what the file looks like and how large it is. Everything else is secondary to it.
CRF: fix the quality, let the size follow
ffmpeg -i in.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k out.mp4
CRF tells the encoder to hold a level of visual quality and spend whatever bitrate that requires. A still interview uses very little; a scene of falling snow uses a great deal.
That is the right behaviour for a website. Every video looks consistent, and none of them wastes space being told to hit a number it did not need.
Lower is better quality and larger. Around 18 is close to indistinguishable from the source, 23 is a reasonable default, and 28 is visibly compressed. Moving by six roughly halves or doubles the size.
The number does not transfer between encoders. CRF 23 in H.264 is approximately CRF 28 in H.265, and VP9's scale is different again. Carrying a familiar value into a new codec is one of the most common ways to conclude that the new codec is worse.
The preset is the other half
-preset controls how hard the encoder works: from ultrafast to veryslow. It does not change the target quality. It changes how efficiently that quality is reached.
A slower preset at the same CRF gives you a smaller file that looks the same. medium is the default, slow is usually worth it for anything you will serve repeatedly, and veryslow costs a lot of time for a small gain.
Going the other way, ultrafast produces files large enough to undo the point of encoding at all. It exists for cases where the encode must not fall behind.
Two-pass: fix the size
ffmpeg -y -i in.mp4 -c:v libx264 -b:v 2M -pass 1 -an -f mp4 /dev/null && \ ffmpeg -i in.mp4 -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k out.mp4
The first pass analyses the video and writes a log; the second uses it to distribute the bitrate well while hitting the target.
This is genuinely better than single-pass at a fixed bitrate, and it takes about twice as long. The question is whether you need a fixed bitrate at all.
For a website, you usually do not. Nothing about serving an MP4 requires a specific size, and forcing one means either wasting space on simple content or starving complex content of the bits it needed. Two-pass is for platform limits and delivery contracts, cases where the number is imposed on you.
Constrained quality: the middle option
The genuinely useful hybrid is CRF with a ceiling:
ffmpeg -i in.mp4 -c:v libx264 -crf 23 -maxrate 4M -bufsize 8M -c:a aac -b:a 128k out.mp4
Quality-driven as usual, but never exceeding a peak rate. That matters for viewers on slow connections, where a sudden high-bitrate scene is what causes buffering even though the average was fine.
Set bufsize to roughly twice maxrate as a starting point. This combination is what most streaming setups actually want. Setting up HLS streaming goes into the rendition ladder built on top of it.
Audio
Audio is a fixed bitrate and it is small. 128k AAC is fine for speech and general use, 192k for music. Opus does better at low rates if the container allows it.
What is worth avoiding is re-encoding audio you did not need to touch, -c:a copy keeps it exactly as it was, which is both faster and lossless. Remuxing and stream copy walks through when that is possible.
How to choose, practically
Take one representative clip. Encode it at CRF 20, 23 and 26. Look at all three and note the sizes.
You will find a point where you stop seeing a difference, and that is your value for that kind of content. It is a far better guide than any recommended number, because it is measured against your material and your eyes.
Performance optimisation walks through making the chosen settings run faster once they are settled.
Judge the result instead of the number
Comparing two encodes by eye is unreliable at the level where the decision actually sits, and there is a measurement for it.
ffmpeg -i original.mp4 -i encoded.mp4 -lavfi "[0:v][1:v]ssim" -f null - 2>&1 | tail -1 ffmpeg -i original.mp4 -i encoded.mp4 -lavfi "[0:v][1:v]psnr" -f null - 2>&1 | tail -1
Both compare the encode against the source frame by frame. They are not a substitute for looking, and they are how you compare two settings without a subjective argument.
Use them to answer one question: at what CRF does the measurement stop improving meaningfully? That point is where further quality is being paid for and not received, and it differs by content, which is why a recommended value is only ever a starting point.
Encode a segment, not the whole file
Testing settings on a full-length video wastes most of the time spent choosing them.
ffmpeg -ss 120 -t 30 -i input.mp4 -c copy sample.mp4
Take a representative thirty seconds, from the middle instead of the opening, and from a section with motion instead of a title card. Then run every comparison against that.
The sample must be cut with a copy rather than re-encoded, or you are comparing settings against an already-degraded source and the results mislead. Remuxing and stream copy deals with why that cut lands on a keyframe.
When the source is already compressed
Most files being re-encoded are not masters. They are already-compressed video from a phone, a screen recorder or a previous export.
That changes the arithmetic. Encoding an already-compressed file at a low CRF preserves its existing artefacts faithfully and produces a large file; encoding it aggressively compounds them.
The practical consequence is that a CRF which looks right on a master is frequently too low for second-generation material, and comparing against the source rather than against an ideal is the only honest test: the measurement above is against what you were given, not against perfection.
The output people actually see
FFmpeg reports its own view of the result, and two lines from it are worth reading rather than scrolling past.
ffmpeg -i in.mp4 -c:v libx264 -crf 23 -preset medium out.mp4 2>&1 | grep -E 'video:|frame='
The final summary states how many kilobytes went to video and how many to audio, which immediately shows whether audio is a meaningful share, on a short clip at a high audio bitrate it frequently is, and reducing it is the easier saving.
The frame line reports the speed as a multiple of real time. A figure well below 1 means the encode is slower than the video's duration, which is the number that matters for a queue. Running FFmpeg jobs in parallel goes into what to do with it.