The obvious implementation - browser posts the file to your API, your API writes it to storage - works until files are large or users are many. Then your application servers are spending their time and memory shuffling bytes, uploads fail on flaky connections with no resumption, and every uploaded file has passed through your application before anyone checked what it was.
01Upload directly to storage
Your backend issues a short-lived presigned URL with constraints on size and content type, and the browser uploads straight to object storage. Your servers handle a small request instead of the whole file, memory pressure disappears, and the storage provider deals with the transfer.
The client then notifies your backend that the upload completed, and you verify server-side that the object actually exists and matches expectations before recording it. Trusting the client's completion callback without verifying is how you end up with database rows pointing at objects that were never uploaded.
| Through the API | Direct to storage | |
|---|---|---|
| Server memory | Proportional to file size | Negligible |
| Server bandwidth | Full file, twice | None |
| Large file support | Poor | Native, with multipart |
| Resumability | Must be built | Provided by the storage API |
| Validation before storage | Possible | Must happen after |
02Constrain the presigned URL properly
A presigned URL is a capability. Scope it tightly: a short expiry measured in minutes, a maximum content length, an allowed content type, and a key path the user cannot influence. Letting the client choose the object key is how one user overwrites another's file.
Generate the key server-side from a tenant identifier and a random component. Never derive it from the user-supplied filename, which is also the class of bug that produces path traversal in the storage layer.
03Validate the content, not the extension
The filename and the declared content type are both user input and both trivially forged. Determine the real type by inspecting the file's magic bytes after upload, and reject anything that does not match what was declared.
For images, re-encoding is the strongest validation available: decode and write out a fresh file. It strips embedded metadata, discards anything hidden after the image data, and produces a file you generated rather than one a stranger did. It also fixes the orientation problems that come from mobile cameras.
| Layer | Catches | Cost |
|---|---|---|
| Size limit on the presigned URL | Storage exhaustion | Free |
| Magic byte inspection | Forged extensions | Low |
| Re-encode images | Embedded payloads, metadata | CPU |
| Malware scanning | Known malicious files | Moderate, adds latency |
| Serve from a separate domain | Stored XSS via uploaded HTML | Free, DNS only |
04Quarantine before it is reachable
If users can upload files that other users download, you are hosting content you did not create for people you cannot vet. Upload to a quarantine bucket, scan asynchronously, and move to the serving bucket only on a clean result.
Serve user content from a separate domain, never from your application's origin. An uploaded HTML file served from your main domain executes with your cookies available to it, which turns a file upload feature into stored cross-site scripting.
05Multipart and resumption for anything large
Above roughly a hundred megabytes, a single request is a bad bet - one dropped connection and the whole transfer restarts. Multipart upload splits the file into chunks that can be retried individually and resumed after an interruption, which on mobile networks is the difference between working and not.
Set lifecycle rules to abort incomplete multipart uploads after a few days. Orphaned parts consume storage indefinitely and do not appear in a normal object listing, which makes them a cost line that nobody can account for.
Topics
Priya Iyer
Staff Engineer · SyncTrix
Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.
Building something like this?
SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.
Talk to an engineer