Skip to main content

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​

CaseMethodPage
One small file, size known up front (avatar, cover image)Presigned PUTPresigned PUT
One small file from a browser form, S3 must enforce the sizePresigned POSTPresigned POST
Several files in one submit (CV plus photos)Batch presigned POST, one policy per filePresigned POST
Large file, or a phone on a weak networkMultipart uploadMultipart upload
User video of any sizeMultipart upload, then transcodeVideo
File the server makes itself (export, thumbnail)Server-side PutObjectnot covered, it is a normal SDK call
Public content (app images, published assets)CloudFront with Origin Access ControlServing files
Private user content (profile photos, documents)Signed GET at read timeServing files
Anything other users will seeModeration before it goes liveModeration

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​

Rule

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.

Rule

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.

Rule

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.

Rule

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.