ESC

Type to search the knowledge base.

Upload Large Files UX

Frontend system design for large uploads — multipart, resume, progress, concurrency, failures, and virus-scan states.

intermediate4 min read
  • system-design
  • interview
  • architecture
  • upload

Scope the problem

Design UX + client architecture for uploading large files (video, datasets, high-res images): reliability on flaky networks, progress honesty, resume, and server processing states.

Out of scope: virus scanner cluster design; define the API contract only.

Requirements

Type Examples
Functional select files, progress, cancel, retry, multi-file
Non-functional survive tab refresh if possible; 1GB+ files
Product completion rate; time-to-available

High-level flow

Validate → request upload session → multipart PUT parts
  → complete session → processing (async) → ready URL

Direct-to-object-storage with signed URLs keeps app servers off the byte path.

API sketch

// 1) init
POST /uploads
{ filename, size, contentType, checksum? }
→ { uploadId, partSize, parts: [{ partNumber, url }] }

// 2) client PUTs each part to signed url

// 3) complete
POST /uploads/:id/complete
{ parts: [{ partNumber, etag }] }
→ { status: "processing" }

// 4) poll or WS
GET /uploads/:id → { status: "ready", assetId } | { status: "failed", error }

Client architecture

UploadManager
├── queue (concurrency N)
├── per-file state machine
├── part uploader (retry)
└── persistence (IndexedDB) for resume

File state machine

queued → hashing? → uploading → completing → processing → ready
                 ↘ failed
                 ↘ canceled
type FileUpload = {
  localId: string;
  file: File;
  uploadId?: string;
  progress: number; // 0..1
  status: "queued" | "uploading" | "processing" | "ready" | "failed" | "canceled";
  error?: string;
  completedParts: number[];
};

Multipart & concurrency

  • Part size e.g. 8–16MB (balance request count vs retry cost)
  • Upload 3–4 parts in parallel per file; limit global concurrency
  • On part failure: exponential backoff; refresh signed URL if expired
async function uploadPart(url: string, blob: Blob, signal: AbortSignal) {
  const res = await fetch(url, { method: "PUT", body: blob, signal });
  if (!res.ok) throw new Error("part failed");
  return res.headers.get("ETag");
}

Progress UX

  • Progress = weighted completed part bytes / total
  • Smooth UI with rAF; don’t thrash React every byte
  • Show speed + ETA estimates carefully (they lie on variable networks)
  • Multi-file: overall + per-file

Resume after refresh

  1. Persist uploadId, part etags, file fingerprint (name+size+lastModified or hash) in IndexedDB
  2. On relaunch, if file still selectable (File System Access API) or user re-picks same file, continue remaining parts
  3. Without FS API, may require re-select file but can skip completed parts if server supports list parts

Validation

  • Max size / MIME allowlist client and server
  • Optional client checksum (SHA-256 web worker) for integrity
  • Image/video duration probes via browser APIs

Cancellation

AbortController per file; call server abort endpoint to stop processing and delete incomplete uploads (cost control).

Post-upload processing

UI states after bytes done:

Status UX
processing spinner “Transcoding…”
ready show asset; enable insert
failed error + retry processing if safe

Use polling with backoff or websocket notification.

Reliability tactics

  • Idempotent complete calls
  • Refresh signatures on 403
  • Detect offline → pause queue (offline indicator)
  • Don’t hold entire file in memory twice; slice File/Blob

Security

  • Signed URLs short-lived
  • Auth on init/complete
  • Don’t echo internal storage paths
  • CSP and malware: show “scanning” if product requires

Performance & mobile

  • Respect data saver; warn on large cellular uploads
  • Lower concurrency on navigator.connection.saveData
  • Background: limited guarantees — use Background Upload APIs where exist; otherwise document “keep tab open”

Tradeoffs

  1. Single PUT vs multipart — size threshold
  2. Client hash cost vs integrity
  3. Strict resume complexity vs retry whole file
  4. Through-server proxy (simpler auth) vs bandwidth cost

Interview close

Init session → parallel signed part uploads with backoff → complete → async processing poll → progress and cancel → IndexedDB resume. Call out direct-to-storage and mobile data warnings.

Further reading