VOD Docs

ADR 0001: Resumable uploads with tus

Why uploads go through tusd and Uppy instead of a single request to the API.

StatusAccepted, implemented
Date2026-07-26
AreaUpload and ingest

Context

Uploads originally went through a single multipart request: the browser streamed the whole file to FastAPI, which forwarded it to MinIO. That had three problems:

  1. Blocking. Every byte passed through the application server, tying up API resources for the length of the upload.
  2. Not resilient. A dropped connection meant starting again from zero.
  3. Not scalable to the target. Files up to 50 GB and several concurrent uploaders were required.

Requirements: 50 GB files, concurrent uploads, resume without resending completed data, bytes kept off the application server, fully self-hosted on the existing MinIO.

Decision

Adopt the tus protocol: tusd as a dedicated ingest service writing to MinIO through its S3 store, and Uppy in the browser. Separate the planes:

  • Data plane: browser → tusd → MinIO. Bytes never reach FastAPI.
  • Control plane: FastAPI takes part only through tusd hooks: it authorises uploads (pre-create) and turns finished uploads into videos (post-finish).
Architecture diagram · placeholder
Control plane vs data plane for uploads

Two lanes: bytes (Uppy → tusd → MinIO) and decisions (tusd ↔ FastAPI hooks).

Alternatives considered

Consequences

Positive

  • Interruptions cost one chunk, not the whole file.
  • Uploads no longer load the API.
  • One component provides both the resumable protocol and efficient S3 multipart writes.
  • The browser talks only to tusd; MinIO is not exposed for uploads.

Trade-offs

  • tusd is one more stateful service, with a volume for its working data.
  • Every byte hops twice (browser → tusd → MinIO), so the ingest host is the bandwidth bottleneck at scale.
  • tusd's upload locks are per instance, so several tusd instances would need sticky routing.
  • pre-create becomes availability-critical: if the API is down, uploads cannot start.
  • post-finish must be idempotent, because hooks can be delivered more than once.
  • The upload token must still be valid when the upload starts.
  • Abandoned multipart uploads accumulate in MinIO and need a cleanup policy.

As built

Follow-up from the ADROutcome
Part size well under S3's 10,000-part limit50 MB parts (tus_part_size_mb), enough for 50 GB in about 1,000 parts
Admission control in pre-create, fail fast with 503 + Retry-AfterImplemented: Redis key per active upload, cap 8, Retry-After: 30
Streaming-friendly proxy settingsflush_interval -1 on /files/*, so tusd's responses (upload offsets, progress) are passed back immediately instead of buffered
Serve finished files from storage, not tusdImplemented: playback goes through /storage to MinIO
Keep the upload token out of stored metadataImplemented: pre-create strips it
Idempotent post-finishImplemented
Lifecycle cleanup for abandoned multipart uploadsNot yet implemented

On this page