# Validate video uploads with ffprobe

Canonical: https://rendobar.com/blog/ffprobe-validate-uploads/
Author: Abdelrahman Essawy
Published: 2026-08-20
Updated: 2026-08-20

---

## Key takeaways

- Of 13 probe jobs that did not complete, 9 could not fetch the input at all and 3 fetched a file named .mp4 that ffprobe could not read as media.
- A probe costs a flat $0.0010. The median FFmpeg encode in the same archive costs $0.0025, so probing first is roughly 40% of one encode.
- Probing is fast enough to sit in front of every job: median 171 ms across 200 completed probes, with a 94 ms to 4,367 ms range.
- Extension and MIME type are both caller-supplied and neither survived contact with real uploads. Only decoding the header tells you what a file is.
- The four checks worth running are: does it parse, does it have the stream you need, is the duration sane, and is the resolution within what you will pay to encode.

## Short version

Run `ffprobe` on the file before you do anything else with it. If it does not parse, it is not media, and nothing downstream will make it one.

We pulled the full probe history out of Rendobar's job archive: **213 ffprobe jobs, 200 completed, 13 did not.** Reading the 13 failures is the whole argument for this post.

- **9** could not fetch the input at all. Dead URL, wrong host, expired link.
- **3** fetched successfully and then failed to parse. Every one of them was named `.mp4`. The bytes were not video.
- **1** was dispatched and never started, which is an infrastructure failure rather than a file problem.

The three in the middle are the interesting group. The filename said MP4. The upload succeeded. Nothing downstream would have caught it before an encoder did.

## The extension is a claim, not a fact

Both signals people reach for are supplied by the caller.

The **file extension** is part of a name the uploader chose. Renaming `report.pdf` to `report.mp4` takes a keystroke, and users do it by accident constantly, usually while trying to "convert" something.

The **Content-Type header** is set by the client. A browser guesses it from the extension. A script sets it to whatever the author typed. Neither has looked at the bytes.

What actually identifies a file is its header. ffprobe reads that header, matches it against every demuxer it has, and tells you what it found along with a `probe_score` for how confident the match is. A well-formed MP4 comes back with `"probe_score": 100`. Something that is not media comes back as an error, not a low score.

## The four checks

Everything worth validating is a field in one probe.

```bash
ffprobe -v error -print_format json -show_format -show_streams input.mp4
```

**1. Does it parse.** If the command exits non-zero, stop. There is no partial credit here. Our three bad uploads all failed at this step, before any field mattered.

**2. Does it have the stream you need.** Walk `streams` and look for a `codec_type` of `video` or `audio`. Do not infer this from the container, because an MP4 can be audio-only and an MP3 with cover art reports a video stream that is a single still image. The tell for cover art is `disposition.attached_pic` set to 1.

**3. Is the duration present and plausible.** `format.duration` can be missing entirely on a stream, and it can be nonsense on a still image, where the `image2` demuxer invents 0.04 seconds. A missing duration is not automatically a rejection, but it does mean you cannot estimate cost before running the job.

**4. Is it within what you will pay to encode.** Resolution, frame rate and bitrate are all in the stream entry. This is where you decide that an 8K 120 fps input is not something your free tier is going to transcode.

## What it costs to check

The economics are the reason to put a probe in front of everything rather than only in front of suspicious things.

A probe on Rendobar bills a **flat $0.0010**, and that number has not moved across the **169 consecutive probe jobs run since 2026-07-20**. It does not scale with file size. A 40 KB JPEG and a six-minute MP3 bill the same.

An FFmpeg job in the same archive has a median of **$0.0025** across its last 425 runs, with a 90th percentile of **$0.0042**. So the probe is roughly **40% of a median encode**, and it eliminates the case where you pay for an encode that was always going to fail.

Speed is not the obstacle either. Across 200 completed probes the median was **171 ms**, the 10th to 90th percentile band was **94 ms to 411 ms**, and the slowest single run was **4,367 ms**. A probe in front of a job adds a fifth of a second to the common case.

## Validating without installing FFmpeg

The probe runs over HTTP with the command as a string, which means the validation step does not need FFmpeg in your application container.

const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });

  // 1. Did it parse at all. A failed probe means it is not media.
  if (job.status !== "complete") return { ok: false, reason: job.error?.message };

  const { summary } = job.output.data;

  // 2. Does it carry the stream we need. 3. Is the duration plausible.
  if (!summary.video) return { ok: false, reason: "no video stream" };
  if (!summary.durationSec) return { ok: false, reason: "no duration" };

  // 4. Is it inside what we are willing to encode.
  if (summary.video.width > 3840) return { ok: false, reason: "too large" };

  return { ok: true, summary };
}`}
  curl={`curl -X POST https://api.rendobar.com/jobs \
  -H "Authorization: Bearer $RENDOBAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ffprobe",
    "params": { "command": "ffprobe -v error -print_format json -show_format -show_streams https://cdn.rendobar.com/assets/examples/sample.mp4" }
  }'`}
/>

The response carries the raw ffprobe report plus a normalised `summary` with the container, duration, resolution, fps, rotation, HDR signalling and audio layout already resolved. For validation the summary is usually enough, because the four checks above map onto four of its fields.

A job that fails carries a message describing which stage failed. In our 13 failures the messages split cleanly into "could not fetch input for probing" for the 9 unreachable URLs and "ffprobe could not read the input" for the 3 files that were not media. Those two strings are different problems with different fixes, and telling a user "we could not download your file" when the truth is "your file is not a video" wastes everyone's time.

## What a probe does not tell you

It reads the header. It does not decode the whole file.

So a probe will happily accept a file whose header is fine and whose frames are corrupt halfway through. It will accept a video that is entirely black. It will accept an audio track that is pure silence. None of those are header problems and none of them show up in `-show_format`.

If you need more than a header check, `-count_frames` decodes packets and gives you a real frame count, which catches truncation. It is genuinely slower, because it walks the file rather than reading a box near the front of it. Reserve it for the cases where the cost of accepting a broken file is higher than the cost of the check.

There is also a security boundary this does not cross. ffprobe parsing a file means a demuxer touched attacker-controlled bytes. Probing is a validation step, not a sandbox, and running it somewhere isolated from your application is a separate decision from running it at all.

## Where this stops

Thirteen failures is a small sample and it comes from our own testing traffic rather than from a production upload funnel with real users attached, so the 9-to-3-to-1 split describes what we happened to break rather than what a public form would see. The direction is what matters: most failures never reached the file, and the ones that did were misnamed.

Every figure here comes from probe jobs already recorded in the job history rather than a fresh benchmark, so the timings carry real production variance. The costs do not vary at all, which is a property of the flat rate rather than a property of the measurement.

Nothing here covers upload-time limits, virus scanning, or the storage side of accepting files. It covers one question, which is whether the thing you just received is the kind of media you think it is.

For the fields that mislead once a file does parse, see [ffprobe metadata gotchas](/blog/ffprobe-metadata-gotchas/). For the full command set with measured cost, see [common ffprobe commands compared](/blog/ffprobe-commands-compared/). For the duration field specifically, which disagrees with itself by up to 493 ms, see [how to get video duration with ffprobe](/blog/ffprobe-video-duration/).
