ffmpegvideo-encodingtroubleshooting

FFmpeg vsync Is Deprecated. -fps_mode Isn't a Drop-In Swap

··10 min read
FFmpeg vsync Is Deprecated. -fps_mode Isn't a Drop-In Swap

For about three years, -vsync is deprecated, use -fps_mode was a warning you could ignore, and most people did. Then FFmpeg 9.0 removed the option outright and the same command started failing with Unrecognized option 'vsync'. The rewrite itself takes ten seconds; the part the warning never tells you is that -fps_mode does not behave like the flag it replaces.

Quick answer: The FFmpeg vsync deprecated warning means -vsync was superseded by -fps_mode in FFmpeg 5.1 and removed in FFmpeg 9.0, so swap -vsync 0 for -fps_mode passthrough, -vsync 1 for -fps_mode cfr, and -vsync 2 for -fps_mode vfr. Numeric values are no longer accepted, -vsync drop has no replacement at all, and because -fps_mode is a per-stream output option where -vsync was global, every output in a multi-output command needs its own copy. If you would rather not version-gate a frame-rate flag across every image you ship, FFmpeg Micro normalizes frame rate on ingest and runs the encode as one API call: https://www.ffmpeg-micro.com/docs.

What each -vsync value becomes

Three of the four old values map one-to-one, and the fourth doesn't map to anything. -fps_mode arrived in FFmpeg 5.1 in 2022, and the FFmpeg documentation describes it as an output, per-stream option that sets the video sync method; -vsync applied to all output video streams and could only ever be overridden per stream by fps_mode.

OldNewWhat actually happens to frames
`-vsync 0` / `passthrough``-fps_mode passthrough`Each frame is passed with its timestamp from the demuxer, untouched.
`-vsync 1` / `cfr``-fps_mode cfr`Frames are duplicated and dropped to hit the requested constant rate.
`-vsync 2` / `vfr``-fps_mode vfr`Timestamps pass through, but frames that would share a timestamp get dropped.
`-vsync drop`nothingTimestamps were destroyed and the muxer regenerated them from frame rate.
`-vsync auto``-fps_mode auto`The muxer picks, which is the default.

The numbers are gone for good. Writing -fps_mode 1 fails with an invalid-argument error on the option rather than quietly meaning cfr, which catches people who did a blind find-and-replace of the flag name and left the argument alone. Use the string names.

This warning reached most developers through tooling rather than through the FFmpeg release notes. The maintainer of FastFlix fixed it in version 5.4.0 after issue #450, where a user ticked CFR on the Advanced tab and got a deprecation notice they hadn't asked for.

-fps_mode is per output, which is why a rewrite can half-work

A command that fans out to several outputs needs the flag written once per output, placed after the last -i and before that output's filename. -vsync was global, so one copy covered everything; drop in a single -fps_mode and only the output it precedes gets it.

ffmpeg -i source.mov -filter_complex \
  "[0:v]split=2[a][b];[a]scale=1080:1920[v916];[b]scale=1080:1080[v11]" \
  -map "[v916]" -map 0:a -fps_mode cfr -r 30 -c:v libx264 -crf 20 out-9x16.mp4 \
  -map "[v11]"  -map 0:a -fps_mode cfr -r 30 -c:v libx264 -crf 20 out-1x1.mp4

Drop the second -fps_mode cfr there and out-1x1.mp4 inherits the muxer default, which for MP4 means auto. The file plays. It just has a different timebase than its sibling, and you find out two steps later when one of them refuses to concat cleanly with the rest of the batch. If you need to be explicit about which stream you mean, the per-stream form works too: -fps_mode:v:0 cfr.

Position matters because -fps_mode is an output option. Put it before -i and FFmpeg treats it as applying to the input, where it has no effect on what you were trying to control.

-vsync drop was deprecated on purpose

There is no successor to drop, and that was deliberate. The FFmpeg patch that deprecated it explains that drop discarded timestamps entirely and relied on the muxer generating new ones from frame rate, and muxer-generated timestamps were themselves on the way out.

If you used drop to force an output to a clean constant rate regardless of what the source claimed, -fps_mode cfr -r 30 is the behavior you actually wanted. If you used it to work around a source with broken or non-monotonic timestamps, fix it at the source instead: -fflags +genpts on the input, or remux the file first. Timestamp garbage that you paper over at the sync stage tends to reappear as the buffer queue overflow and dropping warnings further down a filter graph.

Where CFR belongs in a pipeline

Pin CFR on each clip as it enters the pipeline, not on the final output. That one decision is the difference between a concat that lines up and a 20-minute compilation whose audio sits 400 ms late by the end.

Screen recorders and phones are the usual culprits. OBS, QuickTime screen capture, and most iOS and Android cameras write variable frame rate, where frame timestamps reflect when the frame was actually captured. Concatenate three VFR clips with the concat demuxer and -c copy and FFmpeg has to splice three different timing patterns together; the drift accumulates across the seams, and because the audio keeps its own clean clock, the gap between picture and sound grows the longer the output runs. VideoHelp threads about joining clips with mismatched resolutions and frame rates are mostly this problem wearing a different hat.

Normalize first:

for f in clip-*.mp4; do
  ffmpeg -i "$f" -fps_mode cfr -r 30 \
    -c:v libx264 -crf 20 -pix_fmt yuv420p \
    -c:a aac -ar 48000 -ac 2 "norm-$f"
done
# now the concat demuxer can use -c copy safely
ffmpeg -f concat -safe 0 -i filelist.txt -c copy final.mp4

The same ordering applies to captions. Burn subtitles onto a VFR source and the timing math that was correct in the SRT stops being correct on screen, which is a different flavor of the timing problems that show up converting a transcription API response to SRT. Pin the rate, then burn.

This is also where a hosted step earns its place. FFmpeg Micro normalizes frame rate on ingest, so clips from a dozen phones and three screen recorders arrive at the encode already on one clock and you never write a sync flag at all; the API docs show the upload, submit, poll, download sequence.

Frame extraction wants vfr or passthrough, never cfr

Scene-detection and keyframe extraction scripts are where the FFmpeg 9 removal hit hardest, and they're also the place where the wrong replacement value silently ruins the output. A select filter hands downstream a sparse set of frames; if the sync method is cfr, FFmpeg duplicates those frames to fill a constant-rate output, and you get 900 near-identical JPEGs instead of 40 distinct ones.

ffmpeg -i talk.mp4 -vf "select='gt(scene,0.4)'" -fps_mode vfr frames/%04d.jpg

The breakage surfaced first in AI and agent tooling, not in classic media work. claude-video issue #117 is a frame-extraction script dying on Unrecognized option 'vsync' after an FFmpeg 9 upgrade, and there are matching reports in mcptube and other MCP-shaped video projects. Those pipelines feed frames to vision models, so a run that "succeeds" with 20x duplicate frames costs real tokens before anyone notices.

When the flag doesn't exist at all

Any FFmpeg older than 5.1 rejects -fps_mode with Unrecognized option 'fps_mode', which includes the 4.4 series that Ubuntu 22.04 still ships. So a fleet with one Ubuntu 22.04 runner and one FFmpeg 9 container cannot share a command string, and hardcoding either flag breaks half the fleet.

Probe for it instead of guessing from a version number:

if ffmpeg -hide_banner -h full 2>/dev/null | grep -q -- '-fps_mode'; then
  SYNC="-fps_mode cfr"
else
  SYNC="-vsync 1"
fi
ffmpeg -i in.mp4 $SYNC -r 30 -c:v libx264 -crf 20 out.mp4

Two FFmpeg branches are under active maintenance right now, so this isn't a problem that resolves itself if you wait. FFmpeg 8.1.3 "Hoare" shipped on September 21, 2026, three days after 9.0.2 "Lei", and most Docker base images and static builds still pin 8.x. Version 9.0 also deleted every libnpp filter, so if you're planning that upgrade, the scale_npp removal is the other thing waiting for you.

Pitfalls that cost people an afternoon

Most of the follow-up confusion after a successful rewrite comes from four places:

  • -r 30 on its own asks for 30 fps output but does not force a constant cadence on a VFR source. Use -r 30 -fps_mode cfr together when you need a real CFR file.
  • -c:v copy makes the sync method irrelevant. Stream copy passes packets and their timestamps through untouched, so you cannot fix frame rate without re-encoding the video.
  • NTSC rates are fractional. Write -r 30000/1001 and -r 60000/1001 rather than -r 29.97, or the rounding error shows up as drift in long outputs.
  • -fps_mode after the output filename is a syntax error waiting to happen. Options bind to the file that follows them.

One more thing worth knowing: the -vsync removal is not called out in the FFmpeg 9.0 changelog or the man pages. Jean-Baptiste Kempf's 9.0 writeup lists it alongside -top, -qphist, -filter_complex_script, and -adrift_threshold, and that release summary is closer to a migration notice than anything in the official docs. That gap is exactly why the search for this error string lands you in issue trackers.

FAQ

Is -vsync still safe to use?

-vsync works on FFmpeg 5.1 through 8.x with a deprecation warning and fails on FFmpeg 9.0 and later. Treat any command still using it as a scheduled outage: it breaks the first time someone rebuilds an image without pinning the version.

Do I need -r as well as -fps_mode cfr?

You need both for a predictable result. -fps_mode cfr tells FFmpeg to duplicate and drop frames to maintain a constant cadence, and -r tells it which rate to maintain; without -r, FFmpeg infers the target rate from the input, which is the thing you're trying to stop trusting.

What replaces -vsync drop?

Nothing replaces -vsync drop. The FFmpeg developers deprecated it because it depended on muxers generating timestamps, a capability also being removed, so use -fps_mode cfr -r <rate> if you wanted a clean constant rate, or repair the input timestamps with -fflags +genpts if you were working around a broken source.

Why does my frame extraction produce duplicate frames now?

Frame extraction with a select filter produces duplicates when the sync method is cfr, because FFmpeg pads the sparse frame stream to fill a constant-rate output. Add -fps_mode vfr or -fps_mode passthrough to get one output image per selected source frame.

Can one command work on both FFmpeg 4.x and 9.x?

A single literal command cannot cover both, since FFmpeg 4.x rejects -fps_mode and FFmpeg 9.x rejects -vsync. Build the flag into a shell variable after probing ffmpeg -h full for -fps_mode, which is more reliable than parsing the version banner across distro and static builds.

Version-gating a frame-rate flag across three images is the kind of maintenance that never shows up on a roadmap and never stops either. If you'd rather hand the encode to an endpoint that normalizes frame rate on ingest and hands back a finished file, the API docs walk the full job in six requests, and the free tier gives you 200 tokens, roughly 33 minutes of video, with no card.

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