AWS Builder Center

Direct File Uploads to Amazon S3: A Practical Guide to Presigned Requests

Learn how to send files directly from a browser to a private S3 bucket using a presigned POST, with size limits and a clear validation workflow.

A profile photo, a PDF attachment, or a support ticket image can turn a small API into a file transfer service. In a conventional upload flow, the browser sends the file to your backend, and the backend forwards it to storage.
An alternative is to let the backend authorize the upload while the browser sends the bytes directly to Amazon S3. Presigned requests make that separation possible without giving the browser AWS access keys or making the bucket public.
This article explains the pattern and provides a small Python signing example. It is a building block for an authenticated application, rather than a complete upload service.

Separate authorization from file transfer

Consider an application where signed-in users attach PDFs to support tickets:
  1. The browser asks the backend for permission to upload an attachment.
  2. The backend verifies the user and their access to the ticket.
  3. It creates an object key, records a pending upload, and returns a signed request.
  4. The browser uploads directly to S3.
  5. A validation process checks the received object before the application makes it available.
The backend still controls who can upload and where the file belongs. It simply stops carrying the file payload through the application API. That can reduce backend transfer work, although overall cost and performance still depend on the workload.

Choose between a presigned PUT and a presigned POST

Both approaches support uploads, but their request formats differ:
ApproachClient sendsUseful when
Presigned PUT URLFile bytes in an HTTP PUT requestYou want a straightforward object upload
Presigned POSTMultipart form containing signed fields and the fileYou need policy conditions such as a file size range
For this example, choose a presigned POST. Its policy can restrict the object key, declared content type, and upload size. Do not assume that a basic presigned PUT URL automatically applies the same size conditions.

Keep the bucket private

Create a private general purpose S3 bucket and keep Block Public Access enabled. Give the backend's IAM role only the permissions required for the workflow. The signing component needs s3:PutObject for its permitted upload prefix; a separate validation component needs the read permissions its checks require.
Use the backend's role credentials rather than embedding access keys in the application. Generate object keys on the server, for example:
1
incoming/<upload-id>.pdf
Map the upload ID to the user and ticket in your database. An unpredictable key reduces accidental collisions, but authorization still comes from your backend checks.

Generate a constrained upload form

The following Python helper uses Boto3. Run it on the backend with the bucket and Region configured, and call it only after authentication and authorization. Install Boto3 in your application environment if it is not already available.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
from uuid import uuid4

import boto3

s3 = boto3.client("s3", region_name=os.environ["AWS_REGION"])
bucket = os.environ["UPLOAD_BUCKET"]

def create_pdf_upload():
upload_id = str(uuid4())
key = f"incoming/{upload_id}.pdf"

form = s3.generate_presigned_post(
Bucket=bucket,
Key=key,
Fields={"Content-Type": "application/pdf"},
Conditions=[
{"Content-Type": "application/pdf"},
["content-length-range", 1, 10 * 1024 * 1024],
],
ExpiresIn=300,
)

return {"uploadId": upload_id, "key": key, **form}
The helper requests a five-minute validity window and permits files from one byte through 10 MiB. Boto3 fills in the key-related fields and conditions. Save the upload record before returning the response to the client.
The browser must submit every returned fields entry, followed by the file, as multipart form data to the returned url. Let the browser set the multipart boundary; do not manually set the request header to application/pdf. The signed Content-Type here is a form field describing the object.

Configure browser access with CORS

For a frontend hosted at https://app.example.com, this S3 CORS configuration is a starting point for the POST workflow:
1
2
3
4
5
6
7
8
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["POST"],
"AllowedHeaders": ["Content-Type"],
"MaxAgeSeconds": 300
}
]
Adapt the allowed headers to those your client actually sends. If your application API uses another origin, configure its CORS separately. CORS enables browser interactions; it does not grant S3 permissions or authenticate the caller.

Validate the object before accepting it

A declared content type does not prove the bytes contain a PDF. Treat the upload as untrusted until your validation process checks the actual content and applies any required malware scanning.
You can use S3 object-created notifications to trigger processing. Notifications are delivered at least once and may arrive out of order, so make the validator safe to run more than once. A client-side “upload complete” message is useful for the interface, but should not be your only evidence that a valid object exists.
Do not treat a presigned upload as inherently single-use. A still-valid request may be reused, and writing to the same key can replace an existing object. If you validate and promote files, design that handoff around the exact version or immutable copy you inspected; otherwise a later upload could invalidate the earlier checks.

Plan expiration, cleanup, and costs

Keep the signing response out of logs and analytics: the signature authorizes an operation. Expiration is also bounded by the signing credentials; temporary credentials can expire before the requested validity window ends.
Track pending, accepted, and rejected uploads, and remove abandoned objects according to your retention policy. Estimate S3 storage, requests, any applicable data transfer, and validation compute. Direct uploads change the transfer path; they do not make storage or processing free.
Presigned requests work best when they are one part of a complete workflow: authenticate the user, authorize the destination, constrain the request, and verify the resulting object before using it.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article