Cloudflare Stream 200 MB upload limit: when you must use tus
Cloudflare Stream accepts basic uploads up to 200 MB and requires tus above that. Read the size from a Sume probe and route the file before you upload.

Cloudflare Stream takes a basic multipart POST only for files under 200 MB. Anything bigger has to go through the tus protocol, and Cloudflare recommends tus on shaky connections even below 200 MB. If your pipeline produces clips of varying size, read the file size first and pick the path in code. Sume's unbilled video inspect gives you that size without downloading the file.
Facts from the Cloudflare page
| Item | What the page says |
|---|---|
| Basic POST upload | Maximum 200 MB |
| Above 200 MB | tus is required |
| tus chunk size | 150 * 1024 * 1024 bytes in the example |
| tus header | Upload-Length must be sent when the endpoint is created |
| Duration cap | maxDurationSeconds is set when you create the upload URL |
| Consequence | Your backend must learn the file size before it asks for a tus URL |
Read the size without a download
POST /v1/video-inspect on a media.sume.com clip returns a probe object that includes size_bytes, duration_seconds, width, height and fps. Probe and stills are unbilled; passing frames: false skips the stills. The clip must already be in your workspace, so import it first (see media inputs).
import os, requests
from time import sleep
API = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
def size_bytes(video_url):
r = requests.post(API + "/v1/video-inspect", timeout=60, json={
"video_url": video_url, "frames": False},
headers={**H, "Idempotency-Key": "size-" + video_url[-24:]})
r.raise_for_status()
data = r.json()["data"]
vi = data["video_inspect"]
while vi["resource_status"] == "processing" or vi["status"] == "queued":
sleep(2)
got = requests.get(f"{API}/v1/video-inspect/{data['request_id']}",
headers=H, timeout=60).json()["data"]
vi = got["video_inspect"]
return (vi.get("probe") or {}).get("size_bytes")
def route(video_url):
size = size_bytes(video_url)
if size is None:
return "tus"
return "basic" if size < 200 * 1024 * 1024 else "tus"
If the file is too big
Sume has no bitrate, codec or CRF field: the server rejects them with ffmpeg_fields_rejected. What you can change is length and frame size. Video trim takes start plus end or duration and an output of width/height (256 to 2160) and fps (24, 25, 30 or 60), exact precision only. A smaller frame size usually shrinks the file, but we do not publish a size guarantee, so probe the new MP4 again.
Limits
The sample treats an unknown size as tus, which always works. It does not cover Cloudflare's other limits, such as account storage, and the page does not state a cap for maxDurationSeconds. The loop assumes the data envelope and the resource_status field that the API returns for inspect jobs; handle failed and canceled in real code instead of treating a missing probe as tus.
Sources
Related posts
More in Developers
- Cloudflare Workflows step.sleep and a Sume job: poll without steps
Cloudflare Workflows step.sleep does not count toward the step limit, so a Sume job poll loop can wait cheaply. Limits, a loop shape and the retention catch.
- Codex 0.160 queued messages resume after reconnect: Sume safety
Codex 0.160 resumes unsent queued messages after a reconnect without duplicate sends. That covers messages, not tool effects, so keep Sume idempotency_key.
- Compare AI image models on the same prompt: a 20-line API script
MAI-Image-2.6 and Muse Image both claim No. 2 on Arena. Skip the leaderboard: run one prompt through several Sume image models and compare the URLs and cost.
- Run one prompt on five image models via Sume: 200 vs 202 in Python
A Python script fires one prompt at Seedream, FLUX.2, Qwen, Ideogram and Recraft ids on Sume and handles both the 200 image reply and the 202 job envelope.
Written by Sume