Video bitrate is the one number that sets both how good a video looks and how big the file is. Pick it too low and motion turns into blocks. Pick it too high and every render costs more storage, more bandwidth and a longer upload for no visible gain.
This guide covers the formula that links bitrate to file size, the 1080p and 4K targets YouTube and Vimeo publish, and what the five Shotstack quality presets produce when you measure them.
The last section has a short Python script that renders one clip at every preset and reads the bitrate back from the API, so you can run the same test on your own footage.
medium preset rendered a 1080p clip at 8.2 Mbps, which lands on YouTube’s 1080p target. high and veryhigh both came out at about 18.4 Mbps. verylow produced a larger file than low.Bitrate is the amount of data the encoder spends on each second of video. It is written in kilobits per second (kbps, 1,000 bits per second) or megabits per second (Mbps, 1,000,000 bits per second). Every second of playback is a stream of bits, so the bitrate sets both how much detail each frame keeps and how large the file ends up.
The file size follows from the bitrate and the duration:
file_size_bytes = bitrate_bps × duration_seconds ÷ 8
A ten minute 1080p video at 8 Mbps works out to 8,000,000 × 600 ÷ 8 = 600,000,000 bytes, or 600 MB.
One thing to watch when you calculate this in code: encoders and network tools use decimal units (1 MB = 1,000,000 bytes), while most file systems report binary units (1 MiB = 1,048,576 bytes). The same file reads about 7 per cent smaller in binary units. This guide uses decimal units throughout.
Resolution and frame rate decide how many bits a video needs to look clean.
Video codecs such as H.264 compress the stream by predicting change between frames and within frames, so the bitrate does not scale in a straight line with pixel count. It still has to grow with resolution and frame rate. When it does not, the encoder drops detail, and you see blocking in motion, banding in gradients and smeared edges.
There are two ways to tell an encoder how much data to spend.
-b:v in FFmpeg, often with -maxrate and -bufsize) holds the data rate near a fixed number. The file size is predictable. Static scenes get more bits than they need, and fast motion can run short and show blocks.-crf) holds the visual quality steady and lets the bitrate move. Complex frames get more bits, still frames get fewer. The file size is not known until the encode finishes.Platforms that re-encode every upload tend to prefer the second approach for source files. Vimeo’s compression guidelines recommend setting the constant rate factor to 18 or below, with the H.264 High profile rather than Main. For the FFmpeg side of this, see the guide to constant rate factor encoding and the tutorial on compressing video with FFmpeg.
YouTube and Vimeo both transcode every upload into a ladder of streaming renditions. Your file is the source for that transcode, so it needs enough data that the second encode has something to work with. Both platforms publish numbers.
YouTube’s upload guidelines give target bitrates by resolution and frame rate, in an MP4 container with H.264 video:
| Resolution | SDR, 24 to 30 fps | SDR, 48 to 60 fps | HDR, 24 to 30 fps | HDR, 48 to 60 fps |
|---|---|---|---|---|
| 1080p | 8 Mbps | 12 Mbps | 10 Mbps | 15 Mbps |
| 2160p (4K) | 35 to 45 Mbps | 53 to 68 Mbps | 44 to 56 Mbps | 66 to 85 Mbps |
The high frame rate column is 50 per cent higher than the standard one, not double, because the encoder reuses most of each frame in the next. The 4K row is four to five times the 1080p row, roughly in line with the four times pixel count.
Vimeo’s guidelines ask for a variable bitrate inside a range instead of a single target:
| Resolution | Vimeo variable bitrate |
|---|---|
| 720p | 5 to 10 Mbps |
| 1080p | 10 to 20 Mbps |
| 2K | 20 to 30 Mbps |
| 4K | 30 to 60 Mbps |
The two sets of numbers agree on the shape: around 8 to 20 Mbps for 1080p, 30 to 60 Mbps for 4K. For the dimensions and aspect ratios each platform expects, see the video sizes and aspect ratios reference.
The Shotstack Edit API does not expose encoder flags. The output.quality field takes one of five presets, verylow, low, medium, high and veryhigh, and the API reference describes medium, the default, as the balance of quality, render speed and file size. The reference does not state a bitrate for any preset, so we measured them.
The test rendered the same three second clip at resolution: "1080" (1920 × 1080 at 25 fps) once per preset. It then read each output back through the API’s probe endpoint, which runs ffprobe on the hosted file and returns the container and stream metadata.
The renders ran in production, not the sandbox, so there is no watermark in the picture. The source clip has no audio to speak of, so the figures below are the video stream.
The clip below is one frame split down the middle. The left half is the low render, the right half is high, both from the production runs in the table.
| Quality preset | H.264 profile | Bitrate | File size (3.01 s) | Share of YouTube’s 8 Mbps 1080p target |
|---|---|---|---|---|
verylow | Constrained Baseline | 7.08 Mbps | 2.66 MB | 89% |
low | High | 5.49 Mbps | 2.07 MB | 69% |
medium (default) | High | 8.17 Mbps | 3.07 MB | 102% |
high | High | 18.45 Mbps | 6.94 MB | 231% |
veryhigh | High | 18.38 Mbps | 6.91 MB | 230% |
Three things stand out.
medium lands on the YouTube 1080p target. 8.17 Mbps against a recommended 8 Mbps. If you render at the default and upload to YouTube, you are already at the number Google publishes.high and veryhigh produced the same file. Both came out at about 18.4 Mbps, 2.3 times the size of medium. For this clip the encoder had enough bits at high to keep every detail, and veryhigh had nothing left to add. On footage with more motion or grain the two may separate. For a master file that will be re-encoded, high is the sensible ceiling.verylow is not the smallest file. It came out 28 per cent larger than low. The probe shows why: verylow switches the H.264 profile to Constrained Baseline, which drops the compression tools the High profile uses, so it needs more bits for the same picture. If the goal is the smallest file, low is the preset to test first.For a 4K output, set resolution: "4k" (3840 × 2160 at 25 fps) in the same script and run it again. 4K rendering is available on the plans that include it, so check the pricing page before you test.
The script below renders the clip at all five presets, waits for each render to finish, and reads the bitrate, file size, codec profile and frame rate from the probe endpoint. It needs only the Python standard library.
To run it:
SHOTSTACK_API_KEY.SHOTSTACK_ENV=v1 to run it in production, which is what produced the numbers above."""Render one clip at every Shotstack quality preset and measure the result with the probe endpoint.
Usage: SHOTSTACK_API_KEY=... python bitrate_benchmark.py (sandbox)
SHOTSTACK_API_KEY=... SHOTSTACK_ENV=v1 python bitrate_benchmark.py (production)
"""
import json
import os
import sys
import time
import urllib.parse
import urllib.request
API_KEY = os.environ.get("SHOTSTACK_API_KEY") or sys.exit("Set SHOTSTACK_API_KEY first.")
ENV = os.environ.get("SHOTSTACK_ENV", "stage") # stage = sandbox, v1 = production
BASE = f"https://api.shotstack.io/edit/{ENV}"
SOURCE = "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/beach-overhead.mp4"
LENGTH = 3 # seconds
QUALITIES = ["verylow", "low", "medium", "high", "veryhigh"]
def call(method, path, body=None):
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(BASE + path, data=data, method=method, headers={
"x-api-key": API_KEY, "Content-Type": "application/json", "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["response"]
def render(quality):
edit = {
"timeline": {"tracks": [{"clips": [{"asset": {"type": "video", "src": SOURCE}, "start": 0, "length": LENGTH}]}]},
"output": {"format": "mp4", "resolution": "1080", "quality": quality},
}
return call("POST", "/render", edit)["id"]
def wait(render_id):
while True:
r = call("GET", f"/render/{render_id}")
if r["status"] == "done":
return r["url"]
if r["status"] == "failed":
sys.exit(f"render {render_id} failed: {r.get('error')}")
time.sleep(3)
def probe(url):
meta = call("GET", "/probe/" + urllib.parse.quote(url, safe=""))["metadata"]
video = next(s for s in meta["streams"] if s["codec_type"] == "video")
audio = next((s for s in meta["streams"] if s["codec_type"] == "audio"), {})
return meta["format"], video, audio
ids = {q: render(q) for q in QUALITIES}
print(f"{'quality':<9}{'MB':>7}{'Mbps':>7}{'video kbps':>12}{'audio kbps':>12} codec fps seconds render id")
for quality, render_id in ids.items():
fmt, video, audio = probe(wait(render_id))
size_mb = int(fmt["size"]) / 1_000_000
mbps = int(fmt["bit_rate"]) / 1_000_000
v_kbps = int(video.get("bit_rate", 0)) / 1000
a_kbps = int(audio.get("bit_rate", 0)) / 1000
num, den = video["r_frame_rate"].split("/")
fps = int(num) / int(den)
print(f"{quality:<9}{size_mb:>7.2f}{mbps:>7.2f}{v_kbps:>12.0f}{a_kbps:>12.0f} "
f"{video['codec_name']} {video.get('profile', '')} {fps:g} {float(fmt['duration']):.2f} {render_id}")
The script submits all five renders first, then polls each one. A render returns an id straight away and finishes in the background, so submitting them together saves waiting five times over. The probe call takes the hosted output URL and returns the same JSON ffprobe would give you locally, with no download. The media metadata guide covers the rest of that response.
This is the output from the run behind the table above:
quality MB Mbps video kbps audio kbps codec fps seconds render id
verylow 2.66 7.08 7092 2 h264 Constrained Baseline 25 3.01 8d3907c7-d0df-485d-ae6d-a8d515cba8df
low 2.07 5.49 5498 2 h264 High 25 3.01 cfd23894-5053-409a-ba95-52c6c62e301a
medium 3.07 8.17 8185 2 h264 High 25 3.01 5dc19b33-f962-46fd-971c-7129ac70f2f9
high 6.94 18.45 18490 2 h264 High 25 3.01 26417d43-b9f7-45c8-877d-af29fdd22c73
veryhigh 6.91 18.38 18419 2 h264 High 25 3.01 baf289d8-3219-4ca1-9d68-91754a8a3c2a
Swap SOURCE for your own footage to see how the presets behave on it. Talking heads, screen recordings and drone shots all compress differently, and the gap between medium and high will move with them.
The platform targets give you the number to hit. The measurements give you the preset that hits it. For most 1080p work the default medium preset is already at YouTube’s target, and high is the ceiling worth paying for.
The script above tells you where your own footage lands in a couple of minutes. Sign up for a free API key and run it on your clips.
No. All five renders in the test came back at 1920 × 1080 and 25 fps. The preset changes how many bits the encoder spends, and at verylow it also changes the H.264 profile. Resolution comes from output.resolution or output.size, and frame rate from output.fps, which accepts 12, 15, 23.976, 24, 25, 29.97, 30, 48, 50, 59.94 and 60.
YouTube’s guidelines list 128 kbps for mono, 384 kbps for stereo and 512 kbps for 5.1 surround. Vimeo suggests 320 kb/s. Audio is a small share of the total: a 384 kbps stereo track adds under 5 per cent to an 8 Mbps 1080p file.
No. The output settings are format, resolution or size, aspect ratio, frame rate and quality. There is no bitrate field. If a delivery spec demands an exact figure, render at high and transcode the output with FFmpeg using -b:v, -maxrate and -bufsize, as shown in the FFmpeg compression guide.
For compatibility and speed, not size. The Constrained Baseline profile plays on older hardware decoders and encodes faster than High. If every viewer is on a modern phone or browser, low gives a smaller file at better quality.
curl --request POST 'https://api.shotstack.io/v1/render' \
--header 'x-api-key: YOUR_API_KEY' \
--data-raw '{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {
"type": "video",
"src": "https://shotstack-assets.s3.amazonaws.com/footage/beach-overhead.mp4"
},
"start": 0,
"length": "auto"
}
]
}
]
},
"output": {
"format": "mp4",
"size": {
"width": 1280,
"height": 720
}
}
}'Published October 28, 2026
Peace Aisosa is a Customer Success Specialist at Shotstack. She writes about video automation when she's not troubleshooting customers' video pipelines. More from Peace
