Nguyên tắc mở đầu: file không đi qua API server. Client xin URL ký sẵn (pre-signed URL) rồi upload thẳng lên object storage (S3/GCS/R2). API chỉ cấp quyền và nhận thông báo hoàn tất — nhờ vậy API không bị chiếm băng thông và không cần đĩa.
Luồng:
1. POST /uploads { contentType, size } -> { uploadUrl, assetId }
2. PUT {uploadUrl} (client -> storage truc tiep)
3. POST /uploads/{assetId}/complete
4. storage event -> queue -> worker xu ly
5. asset.status: uploaded -> processing -> ready | failedKiểm soát ở bước 1 là chỗ hay quên: giới hạn dung lượng, giới hạn loại file, và không tin contentType client khai — worker phải kiểm tra magic bytes thật sự trước khi xử lý.
Xử lý nền: ảnh thì tạo vài kích cỡ + chuyển WebP/AVIF; video thì transcode nhiều bitrate và đóng gói HLS. Video nặng nên tách hàng đợi theo độ dài/độ ưu tiên, tránh một file 2GB làm nghẽn hàng đợi thumbnail. Worker phải idempotent theo assetId vì message có thể được giao lại.
File lớn / mạng yếu (rất hay gặp ở app di động VN): dùng multipart upload — chia phần, phần nào lỗi thì gửi lại phần đó, hỗ trợ tiếp tục sau khi rớt mạng. Tải xuống dùng HTTP range request để tua video mà không cần tải toàn bộ.
Phục vụ file: đặt CDN trước storage; ảnh riêng tư dùng URL ký có hạn ngắn.
Điểm nghẽn và đánh đổi:
- Transcode tốn CPU nhất → autoscale worker theo độ dài hàng đợi, không theo CPU của API.
- Rác: nhiều bản ghi uploaded không bao giờ hoàn tất → job dọn theo lifecycle rule.
- Nếu chờ xử lý xong mới cho đăng, trải nghiệm chậm; nếu cho đăng ngay và xử lý nền, phải chấp nhận trạng thái "đang xử lý" hiển thị cho người dùng. Đa số app chọn vế thứ hai.