Which upload method
Users send us photos, documents and videos. This page helps you pick how the file gets from the user's device into S3, and how it gets back out later. Read it before you build any upload.
How it works
The big idea: file bytes never pass through our API. The API only hands out a short-lived permission slip (a presigned URL or policy). The client sends the bytes straight to S3. Then the client tells the API "done", and the API checks the file.
Read it top to bottom. Each box links to a page in this module.
Summary table
| Case | Method | Page |
|---|---|---|
| One small file, size known up front (avatar, cover image) | Presigned PUT | Presigned PUT |
| One small file from a browser form, S3 must enforce the size | Presigned POST | Presigned POST |
| Several files in one submit (CV plus photos) | Batch presigned POST, one policy per file | Presigned POST |
| Large file, or a phone on a weak network | Multipart upload | Multipart upload |
| User video of any size | Multipart upload, then transcode | Video |
| File the server makes itself (export, thumbnail) | Server-side PutObject | not covered, it is a normal SDK call |
| Public content (app images, published assets) | CloudFront with Origin Access Control | Serving files |
| Private user content (profile photos, documents) | Signed GET at read time | Serving files |
| Anything other users will see | Moderation before it goes live | Moderation |
Terms used in this module
- Presigned URL. A normal S3 URL with a signature in the query string. Anyone holding it can do one thing (for example PUT one key) until it expires. It is a bearer token. Treat it like a password with a short life.
- Presigned POST. A form upload. The server signs a policy: a list of rules such as "key must be exactly this" and "size between 1 byte and 5 MB". S3 checks the rules against the real bytes.
- Multipart upload. S3 takes a big file as numbered parts, then joins them.
- Key. The object's path inside the bucket, like
user-uploads/avatars/USER_ID/UUID.jpg. - Confirm step. After the upload, the client calls the API. The API checks the object really exists and has the right size and type. Only then does the row become live.
Rules
Bytes go client to S3 directly. Never stream user uploads through the API. The API would pay for the bandwidth and memory, and load balancer body limits would cap file size.
The server picks the key. Never use the client's file name in a key. File names leak personal data, can clash, and can contain path tricks.
Store the key in the database, never a URL. Make URLs at read time. Signed URLs expire, and a CDN switch changes every public URL. See Serving files.
Every upload has a confirm step and a cleanup path. A presign that is never confirmed leaves an orphan object. A lifecycle rule must delete it.
Mistakes we made
See Mistakes we made for the full list. The ones that shaped this page:
- A form with seven attachments made seven presign calls. Each call cost a bot check and a rate-limit slot. One flaky upload lost the whole submission. Presign all files of one submit in one call.
- The production bucket had no CORS rules. Every browser upload failed. Dev had the rules, so nobody saw it before release.
Checklist
- I picked the method from the table above and can say why.
- The server builds the key. No client file name in it.
- The database stores the key, not a URL.
- There is a confirm call that checks the object with
HeadObject. - A lifecycle rule cleans up uploads that never got confirmed.
- Bucket CORS allows this method from this web origin, in every environment.
- If other users will see the file, it goes through moderation first.