TechEarl

How to Convert a Video to GIF with ffmpeg (High Quality)

Convert an MP4 to a high-quality GIF with ffmpeg using the two-pass palettegen/paletteuse filter. The naive one-liner gives you a banded, bloated GIF; a per-clip palette fixes it. Works the same on Linux, macOS, and Windows.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Convert an MP4 to a high-quality GIF with ffmpeg using the two-pass palettegen and paletteuse filter, plus fps and scale to control the file size.

The high-quality way to turn a video into a GIF with ffmpeg is a two-pass palette: generate an optimal 256-color palette from the clip, then encode the GIF against it. Here is the whole thing:

bash
# Pass 1: build a palette tuned to this specific clip
ffmpeg -i in.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png

# Pass 2: encode the GIF using that palette
ffmpeg -i in.mp4 -i palette.png -lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" out.gif

That is the answer. The rest of this page is why those two commands exist instead of one, and how to tune fps, scale, and the trim so the GIF is both sharp and small.

Why not just ffmpeg -i in.mp4 out.gif?

You can. ffmpeg will happily produce a GIF from a single command:

bash
ffmpeg -i in.mp4 out.gif

The problem is what comes out: a GIF that is visibly banded, dithered into a noisy mess, and several times larger than it needs to be. GIF is a palette-indexed format capped at 256 colors per frame. When ffmpeg has no palette to work from, it falls back to a generic, fixed palette baked into the encoder. That generic palette has no idea your clip is mostly skin tones, or mostly a blue UI, so it spends its 256 slots on colors your video never uses and starves the ones it does. The result is the classic ugly GIF: gradients turn into stripes, and the file is bloated because the dithering adds entropy the GIF compressor cannot squeeze out.

People reach for -b 2048k to fix this, thinking a higher bitrate buys quality. It does nothing useful. GIF is not a bitrate-controlled codec; it is indexed color plus LZW. A video bitrate flag has no meaningful effect on a GIF. The lever that actually matters is the palette, and that is what palettegen gives you.

The two-pass method, explained

palettegen walks the (downscaled, frame-rate-reduced) video and computes the single best 256-color palette for that exact clip. paletteuse then maps every frame onto that palette, dithering only where it needs to. A palette built from your footage beats the generic one every time, because all 256 slots go to colors the clip actually contains.

Pass one writes a tiny palette.png (it is a 16x16 grid of swatches, not a real picture):

bash
ffmpeg -i in.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png

Pass two feeds the source and the palette in as two inputs and wires them together with -lavfi:

bash
ffmpeg -i in.mp4 -i palette.png -lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" out.gif

Read the filtergraph left to right. The source video runs through fps and scale, and the output of that chain is labelled [x]. Then [x] (the processed video) and [1:v] (the second input, the palette) are fed together into paletteuse, which emits the final GIF. The fps and scale settings must be identical in both passes, otherwise the palette is computed for a different image than the one you encode, and quality drops.

If you would rather not leave a palette.png lying around, do it in one command with split:

bash
ffmpeg -i in.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" out.gif

split forks the stream into two identical copies: one goes to palettegen to build the palette [p], the other waits and is then encoded with that palette. Same result, no temp file. The explicit two-file version is easier to debug and lets you reuse one palette across several clips, so I tend to teach it first.

fps: the biggest size lever

A GIF stores every frame as a full indexed image, so frame count drives file size more than anything else. Dropping from 30 fps to 15 fps roughly halves the file and is invisible for most screen recordings and UI demos. For talking-head or fast motion, 20 to 24 fps looks smoother; for a slow UI walkthrough, 10 to 12 fps is plenty.

bash
# Smoother motion, larger file
ffmpeg -i in.mp4 -vf "fps=24,scale=480:-1:flags=lanczos,palettegen" palette.png

# Smaller file, choppier
ffmpeg -i in.mp4 -vf "fps=12,scale=480:-1:flags=lanczos,palettegen" palette.png

Pick the lowest fps that still reads as motion for your content. There is no benefit to matching the source frame rate; nobody is counting frames in a GIF.

scale: width drives everything, and -1 keeps the aspect ratio

scale=480:-1 means "make it 480px wide, and compute the height so the aspect ratio is preserved." The -1 is the part that saves you: you never have to do the height math, and you cannot accidentally squash the video. Want a smaller GIF? Lower the width:

bash
# 320px wide, height auto
... scale=320:-1:flags=lanczos ...

flags=lanczos picks the Lanczos resampler for the downscale, which is noticeably sharper than the default bilinear. Always include it when you are scaling down; it is the cheapest quality win in the whole command. One gotcha: some encoders want even dimensions. If a tool downstream complains, use -2 instead of -1, which rounds the auto dimension to the nearest even number.

Trim first, with -ss and -t

Almost nobody wants the whole video as a GIF. Cut the clip you want before converting, and put -ss (start) before -i so ffmpeg seeks to that point fast instead of decoding from the top:

bash
# Start at 2s, take 3 seconds, then palette-convert
ffmpeg -ss 00:00:02 -t 3 -i in.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" clip.gif

-ss 00:00:02 seeks to two seconds in; -t 3 takes three seconds of footage. Trimming is the single most effective file-size reduction there is: a 3-second GIF is a tenth the size of a 30-second one, and a tight clip is almost always the better GIF anyway. If you need a precise, repeatable cut with audio still attached for some other purpose, trimming and cutting video with ffmpeg covers the -ss placement and the -c copy keyframe tradeoff in full.

Squeezing the file smaller still

After fps, scale, and trim, a few palette knobs help:

  • Fewer colors. palettegen=max_colors=128 (or 64) shrinks the palette. Flat UI captures survive heavy color reduction; photographic footage shows banding sooner.
  • Dithering choice. paletteuse=dither=bayer:bayer_scale=5 uses ordered (Bayer) dithering, which compresses far better than the default error-diffusion (sierra2_4a) because it produces a regular, repeating pattern instead of noise. dither=none is smallest of all and fine for screen recordings of flat color.
bash
ffmpeg -i in.mp4 -i palette.png -lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5" out.gif

If the GIF is still huge after all of that, the honest answer is that GIF is the wrong format. A 5-second MP4 or WebM is a fraction of the size of the equivalent GIF at the same visual quality, and every browser and chat app plays them inline. Reach for GIF only when the target genuinely needs a .gif (an old wiki, an email client, a platform that auto-plays GIFs but not video). When you do need to ship a GIF and it is too big, optimizing and compressing a GIF on the command line with gifsicle (--lossy, --colors) gets you the rest of the way.

For the full set of ffmpeg one-liners (extract audio, crop, rotate, concat, screenshot, container conversion), the ffmpeg command cheat sheet collects them in one reference.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

Tagsffmpeggifpalettegenpaletteusemp4 to gifvideo to gifCLI

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Reverse a Video with ffmpeg

Reverse a video with ffmpeg using the reverse filter for picture and areverse for sound. Why you must write to a new output file, and why you trim before you reverse.

How to Crop a Video with ffmpeg

Crop a video with ffmpeg's crop filter: crop=w:h:x:y from the top-left origin, centered crops with in_w/in_h expressions, square crops for social, and cropdetect to strip black bars automatically.