ffmpegvideo-encodingapi

FFmpeg Two-Pass Encoding vs CRF: When It Actually Helps

·Javid Jamae·9 min read
FFmpeg Two-Pass Encoding vs CRF: When It Actually Helps

You have a hard ceiling to hit: 25 MB for a Gmail attachment, 50 MB for a client's upload form, or a per-video bandwidth budget your CFO picked. CRF gives you great quality and a file size you find out about after the encode finishes. So you go looking for two-pass, and every guide tells you it's "higher quality," which isn't quite the reason you'd want it.

Quick answer: FFmpeg two-pass encoding runs the encoder twice over the same source. The first pass (-pass 1) analyzes complexity and writes a stats file; the second pass (-pass 2) uses that map to spend bits where motion and detail are highest, so the output lands on your target bitrate almost exactly. Use two-pass when you must hit a file size or bitrate ceiling, and use CRF (optionally capped with -maxrate/-bufsize) when you care about consistent quality and the file size is negotiable. If you'd rather not run two encodes, manage stats files, and babysit long jobs on your own box, FFmpeg Micro runs the whole two-pass sequence as one API call.

Two-pass encoding is a size-accuracy mode, not a quality mode

Conventional wisdom says two-pass encoding produces better quality than single-pass. That's partly true, and it hides the actual mechanism. Two-pass beats single-pass ABR (average bitrate), because single-pass ABR is guessing in the dark: it has no idea that a slow talking-head opening is followed by four minutes of confetti, so it overspends early and starves the hard part later. Two-pass fixes the guess.

Against CRF, two-pass usually loses on quality per bit and wins on predictability. CRF is already the quality-optimal mode. It picks whatever bitrate each scene needs to hit your quality target and hands you a file whose size you didn't choose. Two-pass inverts that: you choose the size, and the encoder decides how much quality that buys.

Here's the practical split. If nobody is going to reject your file for being 71 MB instead of 50 MB, use CRF. If something downstream enforces a number, use two-pass.

The `-pass 1` and `-pass 2` commands

Two-pass in FFmpeg is two invocations sharing a stats file. The first writes it, the second reads it. For H.264 with libx264:

ffmpeg -y -i input.mp4 -c:v libx264 -b:v 2600k -preset slow \
  -pass 1 -an -f null /dev/null

ffmpeg -i input.mp4 -c:v libx264 -b:v 2600k -preset slow \
  -pass 2 -c:a aac -b:a 128k output.mp4

On Windows, replace /dev/null with NUL. The -an on pass 1 is deliberate: audio isn't analyzed, so encoding it is wasted work. The -f null tells FFmpeg which muxer to use when the output filename carries no extension.

H.265 uses x265's own parameter instead of FFmpeg's generic flag:

ffmpeg -y -i input.mp4 -c:v libx265 -b:v 1800k \
  -x265-params pass=1 -an -f null /dev/null

ffmpeg -i input.mp4 -c:v libx265 -b:v 1800k \
  -x265-params pass=2 -c:a aac -b:a 128k output.mp4

VP9 in WebM is the one codec where two-pass is close to mandatory. libvpx-vp9's single-pass mode is noticeably weaker, and Google's own encoding guide recommends two-pass for anything you publish:

ffmpeg -y -i input.mp4 -c:v libvpx-vp9 -b:v 1200k \
  -pass 1 -an -f webm /dev/null

ffmpeg -i input.mp4 -c:v libvpx-vp9 -b:v 1200k \
  -pass 2 -c:a libopus -b:a 96k output.webm

Keep every setting that affects analysis identical across both passes: resolution, frame rate, filters, and preset. Change the scale filter between passes and the stats file describes a video you're no longer encoding.

Picking the bitrate for a target file size

The bitrate number in those commands isn't a vibe, it's arithmetic. Multiply your target size in megabytes by 8192 to get kilobits, divide by the duration in seconds, then subtract the audio bitrate.

A 10-minute 1080p webinar that has to fit under 45 MB: 45 × 8192 = 368,640 kbit, divided by 600 seconds = 614 kbps total. Take off 96 kbps for AAC audio and you get roughly 518 kbps of video, so -b:v 500k leaves headroom for container overhead. Shave 2 to 3 percent off your computed number; MP4 muxing, moov atom, and per-packet overhead are real and they always push you the wrong direction.

That 500 kbps for 1080p is also a signal. It's a tight budget, and tight budgets are exactly where two-pass earns its extra encode. Consider dropping to 720p instead of paying for pixels you can't feed. Our post on compressing video for the web covers where that trade sits for each codec.

Two-pass vs CRF vs capped CRF

Three modes are on the table, and most people only compare two of them. Single-pass ABR is the one to drop first, since two-pass ABR does the same job with an actual plan.

ModeCommand shapeYou controlYou find out later
CRF`-crf 23`QualityFile size, peak bitrate
Capped CRF`-crf 23 -maxrate 3M -bufsize 6M`Quality and peakTotal file size
Single-pass ABR`-b:v 2600k`Average size, roughlyWhere the bits went
Two-pass ABR`-pass 1` then `-pass 2`File size, accuratelyQuality

Capped CRF is the mode most teams should reach for before two-pass. It runs once, keeps CRF's quality-first bit allocation, and clamps the peak so a player with a limited buffer never chokes. -bufsize is the buffer window; setting it to about twice -maxrate gives the encoder roughly two seconds of slack. Every adaptive streaming ladder that isn't chasing a fixed file size uses something like this.

Two-pass stays the right answer when the constraint is a total number rather than a rate: an upload cap, a per-asset storage budget, or a CDN egress line item you've committed to. Two encodes for a size guarantee is a fine trade when a rejected upload costs you a support ticket.

Common pitfalls with FFmpeg two-pass encoding

Most two-pass failures trace back to the stats file, not the encoder. FFmpeg writes ffmpeg2pass-0.log into the current working directory by default, and x265 writes x265_2pass.log.

  • Parallel jobs clobber each other. Run four two-pass encodes in the same directory and they all fight over one log file. The output is garbage bit allocation, not a crash, which makes it nasty to debug. Give every job its own name with -passlogfile /tmp/job-$ID.
  • Error reading log file on pass 2. The stats file is missing, unreadable, or from a different encoder. Usually the container or worker that ran pass 1 wasn't the one running pass 2. If you're doing this in containers, FFmpeg in Docker covers why the filesystem, not the Dockerfile, is where this breaks.
  • -crf plus -b:v plus -pass. Passing a CRF value alongside two-pass ABR doesn't give you "two-pass CRF." libx264 takes the rate control mode you asked for last and quietly ignores the rest.
  • Forgetting -an and -f null on pass 1. Not fatal, just wasted CPU and a confusing error when FFmpeg can't infer a format for /dev/null.
  • Leftover log files. A stale ffmpeg2pass-0.log from yesterday's 4K source will happily be read by today's 720p job.

Two-pass also roughly doubles wall clock. Pass 1 is cheaper than pass 2, so expect somewhere around 1.5x to 2x a single CRF encode of the same source and preset. On a laptop that's an annoyance. On a serverless function with a 15-minute ceiling, it's the difference between a finished file and a timeout, which is the same wall people hit when they try self-hosting FFmpeg on a platform with hard execution limits.

Running both passes without managing stats files

The two-pass sequence is orchestration work more than encoding work: run pass 1, keep its stats file next to pass 2, isolate that file per job, retry cleanly if a worker dies mid-sequence, and keep a long job alive while it runs. That's the part that turns a two-line recipe into a service you maintain.

FFmpeg Micro handles that sequence as a single job. You submit the source and your target, poll or take a webhook, and download the output, with no encoder to install and no log files to keep straight. The docs show the job semantics and the encoding options, and the playground lets you try a target-size encode before you write any code. It works the same way from n8n, Make, and Zapier, which matters when you're encoding a queue rather than one file. For a folder of them, see batch transcoding with one workflow.

FAQ

Is FFmpeg two-pass encoding worth it?

Two-pass encoding is worth it when you have to hit a specific file size or average bitrate, and it's usually not worth it otherwise. At generous bitrates the quality gap over a good single-pass CRF encode is small enough that you're paying double the encode time for a number you didn't need.

Can you use two-pass with CRF in FFmpeg?

CRF and two-pass ABR are separate rate control modes in libx264 and libx265, so combining -crf with -pass doesn't produce a hybrid. The closest useful equivalent is capped CRF: -crf 23 -maxrate 3M -bufsize 6M, which keeps quality-based allocation while limiting peak bitrate.

How do I hit an exact file size with FFmpeg?

Compute the bitrate first: target megabytes × 8192 ÷ duration in seconds, minus your audio bitrate, then subtract another 2 to 3 percent for container overhead. Feed that number to -b:v and run two-pass; the result typically lands within a couple percent of your target.

Why does pass 2 fail with "Error reading log file"?

Pass 2 failed because the stats file from pass 1 isn't where FFmpeg expects it, which usually means the working directory changed, the file was cleaned up, or a different container ran each pass. Set an explicit path with -passlogfile and make sure both passes run in the same environment.

Does two-pass encoding work with H.265 and VP9?

Two-pass works with both, with different syntax. libx265 uses -x265-params pass=1 and pass=2, while libvpx-vp9 uses FFmpeg's standard -pass flag and benefits from two-pass more than H.264 does.

If you want a target-size encode without owning the encoder, the stats files, or the retry logic, sign up free and send your first job through the API.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free