How to compress video to a target size

Convert a size target into encoder settings, measured over 120 jobs and 667 probe encodes. Median compression was only 1.41x and 20 jobs refused to encode.

Share

Short version

There are two different questions hiding in “compress to a target size” and the command you want depends on which one you are actually asking.

If the size is a hard requirement, use two-pass with a computed bitrate. It hits the number.

Terminal window
# 10 MB target, 60 second video, 128k audio
# (10 * 8192) / 60 - 128 = 1237 kbit/s video
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 1237k -pass 1 -an -f null /dev/null
ffmpeg -i input.mp4 -c:v libx264 -b:v 1237k -pass 2 -c:a aac -b:a 128k output.mp4

If you actually want it small and still good, use CRF and stop specifying a size.

Terminal window
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output.mp4

The formula is the part people search for. The trap is what it costs you, and that is measurable.

The formula, and what it does not know

Target size in kilobits divided by duration, minus audio, gives you a video bitrate. Two-pass then spends exactly that budget.

The word “exactly” is the problem. Two-pass distributes bits across the timeline intelligently, but the total is fixed by you rather than by the content. A static screencast and a handheld shot of falling leaves get the same budget for the same duration. One of them is wasting most of it and the other cannot possibly look acceptable.

A bitrate target answers “how big” and refuses to answer “how does it look”. CRF answers “how does it look” and refuses to answer “how big”. Neither one answers both, which is why hitting a size target with predictable quality takes more than one encode.

What it takes to hit both

We ran the search that does answer both, and recorded it. 120 completed compression jobs, 667 probe encodes, a median of 6 probes per job and a maximum of 10.

Each probe is a real encode of a sample of the source at a candidate quality level, scored against the original. The search moves the quality level based on what it measured rather than what it predicted, and stops when the result is inside the target. 58 of the jobs scored with VMAF and 50 with SSIMULACRA2, with the metric chosen by media type, and 12 were dry runs that predicted without encoding.

Six encodes to place one setting sounds expensive until you compare it to the alternative, which is a human encoding, looking, adjusting and repeating. The median job cost $0.0128 with a 90th percentile of $0.0431.

Most files compress far less than you expect

The measured compression ratios are the number most likely to change how you think about this.

PercentileCompression ratio
50th1.41x
90th6.48x
max825.7x

The median was 1.41x. Not 10x, not 5x. Slightly less than half again smaller.

That is not the compressor underperforming. It is what happens when the input is already an H.264 file that some other encoder already optimised. The large ratios in the tail came from sources that were genuinely wasteful, and the 825.7x outlier was an image rather than video.

If your mental model is “compression makes videos much smaller”, it was built on unoptimised sources. On anything already encoded sensibly, the honest expectation is tens of percent, not multiples.

Sometimes the right answer is not to encode

20 of 120 jobs returned passthrough, meaning the search evaluated candidates, found that none of them beat the original at acceptable quality, and handed back the source file untouched.

That is one in six. Re-encoding an already-efficient file usually makes it larger, because you are paying a fresh generation of lossy encoding on top of an existing one and adding container overhead. A compressor that always produces output will happily hand you a bigger file and call it success.

Worth building into your own pipeline whether or not you use a service for it: compare the output to the input, and keep the smaller one. It is three lines and it prevents an entire class of silent regression. We wrote about the behaviour separately in the compressor that refuses to compress.

What quality actually landed

Median achieved score was VMAF 86.0. The range ran from 30.7 to 96.3.

Roughly, above 93 reads as visually lossless, above 80 as good, and below 60 as visibly degraded. A median of 86 is the honest middle: clearly compressed if you compare side by side, fine in normal viewing.

The 30.7 floor is instructive. Those were jobs given a hard byte ceiling well below what the content needed. When a size is non-negotiable, quality is what pays for it, and there is no setting that avoids the trade. The value of measuring is that you find out before shipping rather than after.

What that trade looks like, on the same five seconds, at 8x the difference in bytes:

CRF 20, 533 KB
CRF 40, 66 KB
The same source at two quality levels, showing what an aggressive byte target costs in the picture. Encoded on the Rendobar API on 2026-08-21, 5 seconds of the same public sample.

Play both. At normal size and normal attention the difference is smaller than the 8x byte gap suggests, which is exactly why picking a target by eye on a still frame goes wrong in both directions.

Letting the codec be chosen for you

Across the 120 jobs the search picked h264 47 times, avif 41, opus 9, av1 7, jxl 5, hevc 3, plus a handful of others.

That spread is entirely driven by input type, and it is the reason a fixed codec choice underperforms across mixed media. AV1 is the strongest video codec in the set and it was chosen 7 times, because it is slow and most of these jobs did not need it. AVIF dominated the image inputs. Picking one codec for everything means being wrong on most of it.

Doing it over HTTP

The whole search runs as a single job, and the response includes the probe count, the metric used, the achieved score and the verdict, so the decision is auditable rather than opaque.

job.ts
import { createClient } from "@rendobar/sdk";
const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
const job = await rb.jobs.run({
type: "compress.target",
inputs: { source: "https://cdn.rendobar.com/assets/examples/sample.mp4" },
// A named posture, a 1-100 number, or { maxBytes } when size is fixed.
params: { target: "balanced", for: "web" },
});
const d = job.output.data;
// The search reports itself. "passthrough" means nothing beat the original.
console.log(d.verdict, d.codec, d.probes, d.achievedScore);

Install with npm i @rendobar/sdk. jobs.run() submits and waits, so it returns the finished job in one call.

terminal
curl -X POST https://api.rendobar.com/jobs -H "Authorization: Bearer $RENDOBAR_API_KEY" -H "Content-Type: application/json" -d '{
"type": "compress.target",
"inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4" },
"params": { "target": "balanced", "for": "web" }
}'

Returns immediately with a job id. Poll GET /jobs/{id} or register a webhook rather than blocking on the request.

target takes a named posture, a quality number from 1 to 100, or an object such as { "maxBytes": 153600 } when the size really is fixed. Setting dryRun: true returns the predicted size, quality and cost without encoding, which is how the 12 dry-run jobs in this sample were measured.

Where this stops

120 jobs is a decent sample for verdict distribution and a thin one for per-codec conclusions. The 3 HEVC and 5 JXL jobs are anecdotes, not measurements, and nothing here should be read as ranking codecs against each other. Video codec comparison does that properly on matched inputs.

The compression ratios describe our sources, which skew short, small and already encoded. A library of camera-original footage would show much larger ratios and a very different median.

Two metrics are mixed in the quality figures, VMAF for video and SSIMULACRA2 for images, and they are not on the same scale. The median of 86.0 spans both, so treat it as a summary of this workload rather than a VMAF result.

Nothing here measures perceptual quality with human viewers. Every quality claim is a metric score, and metrics disagree with eyes often enough that we have published a case where they ranked the visibly worse option higher.

For per-setting size numbers, see FFmpeg encoding settings measured. For what these jobs cost against other services, see FFmpeg API pricing compared.

Frequently asked questions

How do I compress a video to a specific file size with FFmpeg?

Compute a bitrate from the target: bitrate equals target size in bits divided by duration in seconds, minus the audio bitrate. Then run two-pass encoding with that value as -b:v. It hits the size reliably and says nothing about how the result looks.

What is the FFmpeg command to compress a video without losing quality?

Use CRF rather than a bitrate. -crf 23 with libx264 is a reasonable default and -crf 18 is near visually lossless. CRF targets a quality level and lets the size land wherever the content needs, which is the opposite trade to a size target.

Why is my compressed video larger than the original?

Because re-encoding an already-efficient file adds bits rather than removing them. In our data 20 of 120 compression jobs found no setting that beat the source and returned the original untouched, which is the correct outcome rather than a failure.

How many encodes does it take to hit a size target accurately?

More than one, because the relationship between a quality setting and the resulting size depends on the content. Our search averaged 6 probe encodes per job across 120 jobs, with a maximum of 10, and it measures quality on each probe rather than guessing.

What is a good VMAF score for compressed video?

Above 93 is generally treated as visually lossless and above 80 as good. Our median achieved score was 86.0. Scores below 60 appeared only on jobs given a byte ceiling too small for the content, where quality was sacrificed to meet the size.

Sources

Tags #ffmpeg#compression#vmaf#video-encoding
All posts
Share
  1. FFmpeg API pricing compared Guides for the video API
  2. How to burn subtitles into a video Guides for the video API
  3. FFmpeg encoding settings compared Guides for the video API