Nguyên tắc: không giữ toàn bộ file trong bộ nhớ hay trên disk của API server. Dữ liệu phải chảy qua dạng stream từ client tới storage.
Hai kiến trúc thường trình bày:
1. Presigned URL (ưu tiên) — API chỉ ký một URL upload, client tải thẳng lên S3/R2 rồi gọi callback báo objectKey. Server không nhận byte nào; băng thông và bộ nhớ của API không bị ảnh hưởng. Với file rất lớn dùng multipart upload: ký từng part, client upload song song rồi complete.
2. Proxy bằng stream — nếu bắt buộc đi qua server (cần kiểm tra nội dung, không lộ được storage), parse multipart bằng parser streaming (busboy) và đẩy thẳng stream sang SDK:
const bb = busboy({ headers: req.headers, limits: { fileSize: MAX_BYTES } })
bb.on('file', (name, file) => {
const upload = new Upload({
client: s3,
params: { Bucket, Key: key, Body: file }, // file is a Readable stream
partSize: 5 * 1024 * 1024,
queueSize: 4,
})
upload.done().then(() => res.json({ key })).catch(next)
})
req.pipe(bb)Những điểm hay bị hỏi thêm:
- Bộ nhớ tối đa xấp xỉ partSize * queueSize — đặt hai số này vừa phải, đừng đẩy cả hai lên cao.
- Tránh library buffer toàn bộ file rồi mới upload (multer memoryStorage là ví dụ) — đó chính là chỗ tràn RAM.
- Chặn kích thước ngay ở limits.fileSize và ở reverse proxy (client_max_body_size), không đợi tới tầng ứng dụng.
- Client mất kết nối giữa chừng thì phải abort multipart upload, nếu không phần đã tải sẽ nằm lại và vẫn tính phí lưu trữ.