· 2 min read
Uploading 150 MB Audio Files to S3 Without Killing the Server
A direct-to-S3 multipart upload pipeline for large audio — presigned URLs, client-driven parts, and a worker that finalizes assembly. Zero bytes touch the app server.
Problem
Users upload long audio files (100–300 MB). Streaming them through an Express server OOMs under concurrency and ties up the event loop. We needed large uploads to go straight to object storage.
Result
Server memory flat (~120 MB) regardless of file size; ~300 MB uploads in under a minute on a 1-core box; resumable after drops.
Stack
TL;DR
| Approach | Server memory | Resumable | Verdict |
|---|---|---|---|
| Stream through Express | Spikes, OOMs under load | No | ❌ |
| Client → S3 (presigned multipart) | Flat (signs + finalizes only) | Yes | ✅ |
Architecture
Browser → S3 directly, with the app server only signing requests and finalizing.
Browser Express API S3 / R2
│ 1. request upload │
│ ─────────────────────▶ │
│ 2. presigned multipart │
│ ◀───────────────────── │
│ │
│ 3. PUT part 1…N (presigned) ────────────────────▶│
│ │
│ 4. complete multipart → API │
│ ─────────────────────▶ │ 5. CompleteMultipartUpload
│ │ ─────────────────────────▶│
│ 6. ok + record id │
│ ◀───────────────────── │
Interesting decisions
- Client-driven multipart, server-signed. The browser asks the API for a presigned URL
per part, uploads each, tracks ETags, then posts the full part list back. The server calls
CompleteMultipartUpload. The server never buffers file bytes — memory stays flat. - 8 MB chunks. Small enough to retry a failed part cheaply, large enough that 150 MB is ~19 requests, not 1500.
- Resumable via a persisted
UploadId. Stored on the draft record, so a dropped connection resumes where it left off. forcePathStyle: true+endpointso the same code targets S3 in prod and Cloudflare R2 in dev — one adapter, two backends.
Lessons learned
- Assert the part-count limit. S3 allows up to 10,000 parts; at 8 MB that’s an 80 GB ceiling. Assert client-side so a wrong chunk size doesn’t silently cap you.
- Abort on cancel, not just on error. An incomplete multipart upload holds storage and
costs money. A lifecycle rule (
AbortIncompleteMultipartUpload after 1 day) is the safety net; an explicit abort on user-cancel is the polite path. - Progress UX matters more than speed. Per-part progress turned a “is it frozen?” experience into a trusted one.
Pros & cons
Pros: server memory flat regardless of file size; resumable; S3/R2 parity. Cons: more moving parts on the client; you must handle the abort lifecycle.
Work with me
I build business software like this — fast, with AI-assisted delivery. Open to remote (US/EU) roles and freelance.