# Compress video for Discord under 20 MB, every time

Canonical: https://rendobar.com/blog/compress-video-for-discord/
Author: Abdelrahman Essawy
Published: 2026-09-27
Updated: 2026-09-27

---

## Key takeaways

- Discord raised the free upload limit to 20 MB in August 2026, so an old 8 MB or 10 MB preset now leaves half the byte budget unused.
- Size is a budget you divide by duration. Two-pass encoding spends that budget, while a CRF value cannot promise any size at all.
- Aim a little under the limit. The container and the encoder's own rate control both drift, and a file one byte over does not upload.
- Check a byte ceiling in code, not by eye. The compress.target job either returns a file under the cap or reports infeasible_size, never a quiet miss.

To get a video under Discord's free upload limit, which Discord's help center
puts at 20 MB since August 2026 (up from 10 MB), run a two-pass encode aimed at
about 19 MB. The video bitrate is the byte budget in kilobits, divided by the
duration, minus the audio bitrate. A CRF value cannot promise a size, so on a
real clip it either overshoots the limit or wastes a chunk of the budget.

Discord's page also lists 50 MB for Nitro Basic and up to 500 MB for Nitro, and
notes it is experimenting with larger limits for some users. Everything below
uses the free tier's number. I count 20 MB as 20,000,000 bytes, the smaller of
the two readings, so a file under it passes whichever one Discord uses.

## The test clip

I cut 45 seconds of [Sintel](https://durian.blender.org/) (Blender Foundation,
CC BY 3.0) starting at 4:40, the rooftop and city sequence, and re-encoded it at
CRF 17 to stand in for a screen or phone recording nobody has compressed yet:
1920x818 H.264 High at 24 fps with 192 kbit/s stereo AAC, 47,738,725 bytes.
That is more than twice the limit, so every method below has real work to do.
The local encodes ran on FFmpeg 8.0 with x264 core 165.

## The two-pass formula

Leave about 5% headroom and aim for 19,000,000 bytes. The arithmetic is the
whole trick:

```bash
# 19,000,000 bytes * 8 = 152,000 kbit, over 45.0 s = 3377 kbit/s in total
# minus 128 kbit/s of audio = 3249 kbit/s for video
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4)
vk=$(awk -v d="$dur" 'BEGIN { printf "%d", 19000000 * 8 / 1000 / d - 128 }')

# Same -preset in both passes. On Windows use NUL instead of /dev/null.
ffmpeg -y -i input.mp4 -c:v libx264 -preset medium -b:v ${vk}k -pass 1 -an -f null /dev/null
ffmpeg -i input.mp4 -c:v libx264 -preset medium -b:v ${vk}k -pass 2 \
  -c:a aac -b:a 128k -movflags +faststart discord.mp4
```

The first pass reads the whole clip to learn where the hard scenes are. The
second spends the budget there. `-movflags +faststart` moves the index to the
front of the file so the embed can start playing before the download finishes.

On the Sintel clip the result was 19,040,572 bytes. That is about 40 KB over what I
aimed for and still under the limit, which is exactly why the headroom exists.
Aim at 20,000,000 and a drift that size puts the file over.

## Why a single CRF guess misses

CRF holds quality steady and lets the size float, so the size you get depends
on the content. The same clip at three CRF values:

| Encode (x264 medium, 128 kbit/s AAC) | Bytes | Under 20,000,000? |
|---|---|---|
| CRF 23 | 22,554,025 | No |
| CRF 26 | 16,108,501 | Yes, with 3.9 MB unused |
| CRF 28 | 13,028,201 | Yes, with 7.0 MB unused |
| Two-pass at 3249 kbit/s | 19,040,572 | Yes |

CRF 23, the x264 default, produced a file Discord would refuse. CRF 26 fits but
leaves a fifth of the budget on the table, and the next clip you feed it will
land somewhere else again. Finding the right CRF for a given clip means encoding
it several times, which [the target-size guide](/blog/compress-video-target-size/)
scripts as a bisection. For a hard ceiling, bitrate is the right tool, and
[CRF against bitrate](/blog/crf-vs-bitrate-measured/) measures that trade in
more detail.

## Where the DIY route breaks

Two-pass is a good answer for one file on your own machine. It gets awkward
when the videos arrive from users or a bot:

- Duration has to be probed first, and a broken duration in the container
  gives you a nonsense bitrate.
- Two-pass lands near the target, not under it. Somebody has to check the
  output size and re-run with a lower bitrate when it lands over.
- A clip that is already under 20 MB should not be re-encoded at all, since
  that costs time and a generation of quality for nothing.
- Long clips need a lower resolution, not only a lower bitrate, and the formula
  does not tell you when you have crossed that line.

## One call with a byte ceiling

Rendobar's `compress.target` job takes the limit itself as the target. With
`{ maxBytes }` it encodes to that budget, runs one corrective pass if the first
encode lands over, and scores the result against the source with VMAF. If the
file still cannot fit, `status` comes back `infeasible_size` so your code sees
the miss. A video already under the cap is returned untouched. This is the
request I ran on the Sintel clip:

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

const job = await rb.jobs.run({
  type: "compress.target",
  inputs: { source: "https://cdn.rendobar.com/assets/examples/sintel-discord-45s.mp4" },
  // Discord's free limit, read as the smaller 20,000,000 bytes.
  params: { target: { maxBytes: 20000000 }, for: "web" },
});

// output.data is typed unknown in the SDK. The job reports its own result.
const d = job.output.data as { status: string; codec: string; achievedScore: number; outputBytes: number };

// "ok" means the file is under the cap. "infeasible_size" means it could not fit.
console.log(d.status, d.codec, d.outputBytes, d.achievedScore);`}
  curl={`curl -X POST https://api.rendobar.com/jobs \\
  -H "Authorization: Bearer $RENDOBAR_API_KEY" \\
  -H "Content-Type: application/json" \\
  -d '{
    "type": "compress.target",
    "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sintel-discord-45s.mp4" },
    "params": { "target": { "maxBytes": 20000000 }, "for": "web" }
  }'`}
/>

Job `job_a2a34750ada64082` returned H.264 at 19,399,054 bytes with status `ok`,
2.46 times smaller than the source, and scored VMAF 97.2 against it. It picked
H.264 by itself, which Discord's page lists alongside HEVC and AV1 as a video
codec it accepts, and kept the index at the front of the file. The job took
74 seconds and cost $0.05.

_The same 360x240 crop, 12 seconds in, at full resolution. Left: the source. Right: the output of job_a2a34750ada64082, under Discord's 20 MB limit._

Two-pass on my laptop cost nothing and landed closer to the byte target, so for
a one-off clip you are sending to a friend, the command above is enough. The job
earns its keep when the upload happens in code: a bot that clips highlights, a
pipeline posting renders to a channel, anything where nobody is around to check
the size and try again. The same job sits beside Rendobar's
[FFmpeg API](/ffmpeg/) when you would rather send your own two-pass command.
And re-encoding is not always the win it looks like, which
[the compressor that refuses](/blog/compressor-that-refuses/) shows on a file
that was already efficient.

WhatsApp has a lower ceiling and different rules about what plays, covered in
[compressing video for WhatsApp](/blog/compress-video-for-whatsapp/). For
Discord, I would budget from 19 MB, run two-pass by hand for a single file, and
hand the ceiling to `compress.target` the moment the uploads are automated.
