Your video host caps GIF previews. Generate them from an API.

A user drops a 40-minute MOV into your S3 bucket, and your product page needs a looping GIF preview and three stills before it renders. The obvious move is to let your video host make them, which works until you read the fine print: the preview exists only if the video lives in their storage, and the animation stops at about ten seconds and 640 pixels wide.
Quick answer: To generate a GIF from a video API-first, keep the file in your own bucket and point two jobs at the same signed URL. One job isoutputFormat: "gif"with-vf "fps=12,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", the otheroutputFormat: "jpeg"with-vf "fps=1/10,scale=320:180,tile=3x3"for a storyboard of stills. Run both with your own FFmpeg binary, or POST both to FFmpeg Micro's/v1/transcodesendpoint, poll each job id untilcompleted, and write both outputs back to the upload's bucket. Billing is per second of input processed, so a GIF trimmed to six seconds of a 40-minute upload bills six seconds, and failed jobs bill nothing.
Preview APIs hand you a GIF only after you hand them your video
Hosted platforms do generate previews, once the video becomes their asset. Mux serves animated images as a GET against image.mux.com built from a playback ID, capped at 10 seconds, 640px and 30fps. imgix's video-generate= makes animations, thumbnails and spritesheets, defaulting to 5 seconds at 15fps and 320px, with the same 640px ceiling. Cloudflare Stream's animated thumbnails are URL attributes, duration defaulting to 5 seconds and fps to 8. Bunny Stream writes preview.gif and thumbnail_1..5.jpg automatically, for video you host with Bunny.
Previews do come free with your video host, at a price: you move your storage, you inherit a ceiling you can't raise, and the URLs live on someone else's domain.
A preview isn't a derivative of an asset. It's a derivative of an input URL and a filter graph, and any URL your storage can sign will do. The ceiling is whatever you type into -vf.
One input URL, two jobs: generate a GIF and thumbnails from the same upload
Three moving parts, no new infrastructure. Your storage fires its upload event, your handler signs a read URL for the new object, and you submit two jobs against that URL. A GIF and a tiled JPEG are different output formats, and one FFmpeg invocation produces one, so that's two jobs. The wait is the slower one, not the sum, because they run in parallel.
The GIF job
No generated palette is the main reason preview GIFs come out enormous: the default 256-color conversion dithers badly and inflates the file. Build the palette from the clip with palettegen, apply it with paletteuse, one graph.
The raw command:
ffmpeg -ss 00:00:05 -t 6 -i upload.mp4 \
-vf "fps=12,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5" \
-loop 0 preview.gif
The same job as one API call, no binary on your box:
curl -X POST https://api.ffmpeg-micro.com/v1/transcodes \
-H "Authorization: Bearer $FFMPEG_MICRO_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{ "url": "https://your-bucket.s3.amazonaws.com/uploads/clip.mp4?X-Amz-Signature=..." }],
"outputFormat": "gif",
"options": [
{ "option": "-ss", "argument": "00:00:05" },
{ "option": "-t", "argument": "6" },
{ "option": "-vf", "argument": "fps=12,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5" },
{ "option": "-loop", "argument": "0" }
]
}'
The response is a job id with status: "pending". Nothing caps at 10 seconds: duration is -t, width is scale. For a 20-second, 720px preview, change two numbers.
The Video to GIF blueprint runs this palette path on your own file, so you can test fps, width and length on a real clip.
The stills job
The still set is the same input URL with a different filter graph. fps=1/10 samples a frame every ten seconds, scale=320:180 sizes each cell, and tile=3x3 packs nine frames into one JPEG your UI can slice.
{
"inputs": [{ "url": "https://your-bucket.s3.amazonaws.com/uploads/clip.mp4?X-Amz-Signature=..." }],
"outputFormat": "jpeg",
"options": [
{ "option": "-vf", "argument": "fps=1/10,scale=320:180,tile=3x3" },
{ "option": "-frames:v", "argument": "1" }
]
}
For separate files, submit one job per timestamp with -ss, -frames:v 1 and -q:v 2, the recipe on the thumbnail guide. To avoid a frame always five seconds in, run FFprobe and seek to duration / 2.
The polling loop
FFmpeg Micro has no completion webhook for callers. You submit a job, poll GET /v1/transcodes/{id} until status is completed, then read output_url. Status checks, listing and downloads are free, so polling costs only HTTP requests.
import time, requests
def wait_for(job_id, key, timeout=900):
headers = {"Authorization": f"Bearer {key}"}
deadline = time.time() + timeout
while time.time() < deadline:
job = requests.get(
f"https://api.ffmpeg-micro.com/v1/transcodes/{job_id}",
headers=headers, timeout=30,
).json()
if job["status"] == "completed":
return job["output_url"]
if job["status"] == "failed":
raise RuntimeError(job.get("error", "job failed"))
time.sleep(3)
raise TimeoutError(job_id)
The only webhook here is on the upload side: S3 event notifications, a Supabase storage trigger, a Drive watcher, a form POST. Everything after it is submit and poll, so this runs the same from a Cloudflare Worker, a Supabase Edge Function with no ffmpeg on the box, or an n8n node.
Wiring it to the upload event in n8n, Make, and Zapier
No-code tools pay off hardest here, since you can't install a binary at all. More than 300 builders run FFmpeg Micro from Make or n8n, and the n8n community node has passed 1,800 downloads.
The n8n chain:
- Storage trigger: AWS S3, Google Drive, Supabase, or a Webhook node your app POSTs to.
- A Set node builds the signed URL.
- Two HTTP Request nodes fire the GIF and stills payloads.
- A Wait node, then an HTTP Request on
/v1/transcodes/{id}. - An IF node loops back to Wait while status is
pending. - Upload both outputs to storage.
The n8n Cloud walkthrough ships this as four workflow files, including a "wait for transcode" sub-workflow, the part people get wrong.
One rule: never let the video bytes into the workflow, pass the URL. Holding a 1 GB upload in an execution is how you OOM a worker. Templates that shell out to a static FFmpeg build can't run on n8n Cloud at all, and n8n 3.0 removes the Read/Write Binary File operations, disables unverified community packages, and drops the task runner timeout to one minute. If you're on that path, fix your video step in this order and see why community nodes stop loading.
Make is shorter: Google Drive "Watch Files" or a custom webhook, then the official FFmpeg Micro app modules, which handle submit and poll as drag-and-drop steps. The five Make scenarios include a parallel render fan-out with the same two-branch shape. In Zapier: a Dropbox or Drive trigger, Webhooks by Zapier (POST) per job, a Delay step, then Webhooks (GET) for status.
A builder hit this wall in n8n's "Help me Build my Workflow" forum on October 26, 2026: a YouTube pipeline on n8n Cloud, job JSON passed through Google Drive, and a separate Windows box running Python and FFmpeg that wrote a render_done receipt with a SHA256 for n8n to detect. The FFmpeg API replaces that box and the receipt with one POST and one GET.
What a GIF plus a thumbnail set costs per upload
FFmpeg Micro bills one token per 10 seconds of input video actually processed, metered per second with no rounding up to the minute, and never bills a failed job. A 60-second UGC upload with three derivatives:
| Job | Key options | Input seconds billed | Tokens |
|---|---|---|---|
| 6s GIF preview | `-ss 00:00:05 -t 6` | 6 | 0.6 |
| 3x3 storyboard | `-vf fps=1/10,...,tile=3x3` | 60 | 6.0 |
| Poster frame | `-ss 00:00:05 -t 1 -frames:v 1` | 1 | 0.1 |
That's 6.7 tokens per upload: the 200 free tokens a new account gets, no card, cover roughly 29 such uploads. On Starter, $19.99 for 2,500 tokens, each upload runs about 5.4 cents, and the grant covers around 370. Top-ups are 500 tokens for $5 on any plan, including Free.
The storyboard dominates because fps=1/10 reads the whole file. If nine stills across the first 30 seconds are enough, add -t 30 and fps=0.3: the job drops from 6 tokens to 3.
Pitfalls that show up on real uploads
Most failures here are plumbing, not FFmpeg:
- Presigned URL expiry. A 15-minute signature on a queued job is a race. Sign for an hour.
- No palette, huge file. UtilityKit measured a 5-second screen recording at 8.4 MB as GIF, 1.1 MB as animated WebP and 211 KB as MP4. If your player accepts it, emit WebP or a looping silent MP4 and keep GIF for email and Slack.
-ssplacement. Before-iit seeks and is fast. After-iit decodes and discards everything before that point.- A short upload against
fps=1/10. A 25-second clip yields 3 frames, sotile=3x3pads the grid with green or gray. Read duration with FFprobe and compute the sample rate. - A 16:9 GIF in a 9:16 card. Crop or pad in the same graph, before
split:crop=1080:1080for a 1:1 card, orscale=608:-1,pad=608:1080:0:(oh-ih)/2:blackfor 9:16. Keep dimensions even; odd widths break an H.264 companion preview. - iPhone MOV rotation. Portrait recordings carry a rotation matrix some filter chains ignore, so the preview comes out sideways. Check
ffprobeside data and addtranspose. - The free-tier input cap. Free accounts accept inputs up to 100 MB, Starter 1,024 MB, Pro 2,048 MB. A 40-minute 4K upload needs a paid plan or a cheap pre-trim.
When this is the wrong shape
Two jobs against your own storage isn't always right. If your video already lives in a hosted player you're committed to and a 5-second 640px animation satisfies the design, use that platform's built-in URL derivative. Scrubbing thumbnails drawn inside a player timeline are a streaming platform feature, not an image job. And if a preview must exist within a second of a cold upload, this API is asynchronous: jobs queue, and expect seconds to a couple of minutes depending on input length.
What you get instead: no ceiling on duration or width, outputs back in their original bucket, and a bill tied to seconds processed.
FAQ
Can one API call return both a GIF and a thumbnail URL?
One FFmpeg Micro job produces one output format, so a GIF and a JPEG are two jobs against the same input URL. That's still one step in practice: two POSTs, two job ids, one polling helper, and they run in parallel.
Do I have to upload the video to the API first?
No. FFmpeg Micro accepts a plain URL in inputs: a presigned S3, Supabase, Cloudflare R2 or Google Drive link, so the file never leaves your storage. A presigned upload path exists if you'd rather hand over the bytes.
How do I generate a GIF preview longer than 10 seconds?
Set -t to the length you want and scale to the width you want. The only limits here are the ones you type into the filter graph. The 5-to-10-second, 320-to-640px ceilings come from hosted-player image endpoints, not FFmpeg.
What happens if the job fails on a corrupt upload?
A failed job returns status: "failed" with an error, and failed jobs are never billed. Usual causes: a truncated file from an interrupted upload, an expired signed URL, a container FFmpeg can't demux. Probe the input and treat a failed probe as a rejected upload.
Point your next upload event at two jobs and see what the pair costs on your own files. A free account comes with 200 tokens, about 33 minutes of input video, no card, at sign up. From your AI agents, the same two jobs are tool calls on the MCP server.
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.
You might also like
Your video thumbnail sprite sheet is fine. The VTT cues drift.
Build a thumbnail sprite sheet video preview with FFmpeg, then generate the WebVTT from the same interval so your scrub-bar cues never drift.

n8n execution crashed, startedAt null? It's a queue mode race
n8n execution crashed with startedAt null is a 2.45.0 queue mode race, not your workflow. Fix: upgrade to 2.45.1+, then stop blocking the worker event loop.

Gemini 4 Argon video costs double Dec 1. Shrink the input.
Gemini 4 Argon video input doubles to $4 per 1M tokens on Dec 1, 2026. Five FFmpeg prep levers that cut the token bill, plus the one-call API equivalent.
Skip the command line
The Video to Animated GIF Converter blueprint runs the same conversion for you: upload the video, pick the length and width, download the GIF.
Run it (free)