# How to get video duration with ffprobe

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

---

## Key takeaways

- The one-liner is ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4, which prints the number and nothing else.
- Container duration and video-stream duration disagreed on all 4 MP4 files in our sample that carry both video and audio. The single video-only MP4 agreed exactly.
- In every one of those 4 files, format.duration equalled the AUDIO stream duration to the last decimal, never the video stream duration.
- The gap ran from 8 ms to 493 ms. On a 30.5 second clip the video stream ended 493 ms before the container claimed the file did.
- Across 200 completed probe jobs the median ran 171 ms and the range was 94 ms to 4,367 ms, while the last 169 all billed exactly $0.0010.

## Short version

Use this:

```bash
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4
```

That prints one number and nothing else. It is the duration a player will show.

It is also not the only duration in the file, and the others do not agree with it. We went back through 200 completed ffprobe jobs already recorded in Rendobar's job history and pulled every one that reported both a container duration and per-stream durations. Eleven distinct media files came out of that. **Four disagreed. All four were MP4 files carrying both a video and an audio stream. The one video-only MP4 agreed exactly.**

## The three durations

`format.duration` is the container's number. It is what `-show_format` prints, what players show, and what the one-liner above returns.

Each stream also carries its own `duration`, which is what `-show_streams` prints per stream. A file with video and audio has at least two of these.

Then there is the number nobody reads by default, the count you get by decoding frames instead of trusting a header. That needs `-count_frames`, and it costs real time because ffprobe walks the packets rather than reading a box near the front of the file.

Three numbers, three different questions. Most duration bugs are asking one and reading another.

## What the files actually said

Every gap in the sample points the same way.

| File | Container | Video stream | Audio stream | Gap |
|---|---|---|---|---|
| `sample.mp4` | 5.013 s | 5.000 s | 5.013 s | 13 ms |
| `sea.mp4` | 3.008 s | 3.000 s | 3.008 s | 8 ms |
| `clip.mp4` | 3.008 s | 3.000 s | 3.008 s | 8 ms |
| `file_example_MP4_480_1_5MG.mp4` | 30.526667 s | 30.033333 s | 30.526667 s | 493 ms |
| a video-only MP4 | 10.000 s | 10.000 s | none | 0 ms |

In all four disagreements, **the container duration equals the audio duration exactly, to the last decimal place**. Not approximately. The same digits.

That is the mechanism and it is not an ffprobe quirk. The container reports the length of its longest stream, and in an MP4 with AAC the audio is longer than the video almost by construction. AAC encodes in fixed 1,024-sample frames, so the encoder pads the final frame out to a full one and adds priming samples at the start. The video stops on a frame boundary at 24 or 30 fps. The two rarely land together.

## Why 493 ms is the number to worry about

Eight milliseconds is under a frame at 24 fps and nothing will notice. 493 ms is twelve frames.

Trim to `format.duration` on that 30.5 second file and you keep 493 ms of audio playing over nothing. Concatenate two such clips and every join carries the same half second of black or frozen frame, and the drift compounds down the timeline. Build a subtitle track against the container duration, burn it against the video, and the last cue lands past the last frame.

The rule that falls out of this: **read `format.duration` when you are telling a human how long something is, and read the video stream duration when you are cutting.** They answer different questions, and in our sample they never agreed on a file with sound.

## The commands worth memorising

Across the archive, 200 completed probe jobs ran 25 distinct ffprobe command shapes. These four cover most of what people actually need.

```bash
# Duration only, machine readable. Nothing but the number.
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4

# Video stream duration, the one that matters when cutting.
ffprobe -v error -select_streams v:0 -show_entries stream=duration -of csv=p=0 input.mp4

# Everything as JSON, for looking rather than parsing one field.
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

# Frame-exact. Decodes packets, so it is the slow one.
ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 input.mp4
```

Probe execution across those 200 jobs had a median of **171 ms**, a 10th-to-90th spread of **94 ms to 411 ms**, and a slowest run of **4,367 ms**. That 46x spread is not ffprobe being unpredictable. It tracks how much of the file had to come over the network before the header was readable, which is why a probe against an MP4 with a front-loaded `moov` atom is fast and one against a file that buried it at the end is not.

Billing followed a floor rather than the work. Every one of the **169 probe jobs run since 2026-07-20 billed exactly $0.0010**, with no variance at all, whether the input was a 40 KB JPEG or a six-minute MP3.

## Reading it from a script

The CSV form exists so you do not have to parse JSON for one number.

```bash
DURATION=$(ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4)
echo "$DURATION"   # 5.013000
```

`-v error` suppresses the banner, which otherwise lands on stderr and confuses anything reading both streams. `-of csv=p=0` drops the section header, so you get `5.013000` rather than `format,5.013000`.

Two failure modes are worth a guard. Some containers report `N/A` instead of a number, and streamed input often has no duration at all until enough of it has been read. Both come back as the literal string `N/A` rather than an empty result, so a naive float parse throws instead of returning zero.

## Running it without installing ffprobe

Rendobar's FFmpeg API takes the same command over HTTP and returns both the raw ffprobe report and a normalised summary, so the container-versus-stream distinction is already resolved in the response.

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

const job = await rb.jobs.run({
  type: "ffprobe",
  params: {
    command:
      "ffprobe -v error -show_entries format=duration -of csv=p=0 https://cdn.rendobar.com/assets/examples/sample.mp4",
  },
});

// The normalised summary resolves the container-versus-stream question for you.
console.log(job.output.data.summary.durationSec); // 5.013
console.log(job.output.data.streams[0].duration); // "5.000000"`}
  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 -show_entries format=duration -of csv=p=0 https://cdn.rendobar.com/assets/examples/sample.mp4" }
  }'`}
/>

The command runs as written. Defaults for JSON output, format, streams and chapters fill in only for flags you did not pass, so a hand-typed command behaves the way it does locally.

## Where this stops

Four disagreeing files is a small sample, and all four are MP4 with AAC audio. The mechanism generalises, because a container reports its longest stream and AAC padding makes audio the longest stream, but we have not measured MKV, WebM, MOV or fragmented MP4, and we have not tested Opus or Vorbis, where the padding behaviour differs.

Every number here comes from probe jobs already in Rendobar's job history rather than from a fresh benchmark run, so the timings carry real production variance. Nothing in the duration comparison depends on timing. Durations are properties of the file and repeat exactly.

The 493 ms figure is one file. Treat it as evidence that the gap gets large enough to see, not as a typical value.

For the wider set of ffprobe fields that mislead, 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/).
