How to Generate an AWS S3 Presigned Upload URL (boto3)

A presigned URL lets a client upload straight to S3 without your credentials. The two bugs that cause a 403 are using get_object instead of put_object, and an ExpiresIn that's too short. Here's the correct boto3 code.

Backend Engineerawss3boto3

What a presigned upload URL is

Your backend signs a temporary URL that grants permission to PUT one object to S3. The client uploads directly to S3 - your server never proxies the bytes, and your AWS credentials never leave the backend. Perfect for mobile/web file uploads.

The correct boto3 code

import boto3
s3 = boto3.client("s3")

def presign_upload(bucket: str, key: str) -> str:
    return s3.generate_presigned_url(
        ClientMethod="put_object",      # WRITE - not get_object
        Params={"Bucket": bucket, "Key": key},
        ExpiresIn=3600,                 # seconds the URL is valid
    )

The client then uploads with a plain HTTP PUT:

curl -X PUT --upload-file photo.jpg "<presigned-url>"

The two bugs that cause a 403 on upload

  1. ClientMethod="get_object" - signs a URL for reading, so a PUT is denied. Uploads need put_object.
  2. ExpiresIn=0 (or too small) - the URL is expired the instant it's issued, so every upload 403s. Set a realistic window (e.g. 3600s).

Both produce the same symptom (403 on PUT), so check the method and the expiry first.

Things that bite people

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why is my S3 presigned upload URL returning 403?

Two usual causes: the URL was signed with get_object instead of put_object (so writes are denied), or ExpiresIn is 0/too small so the URL is already expired. Also ensure any Content-Type the client sends was included when signing.

How do I create a presigned upload URL with boto3?

Call s3.generate_presigned_url(ClientMethod='put_object', Params={'Bucket': b, 'Key': k}, ExpiresIn=3600) and have the client PUT the file to the returned URL.

Does the client need AWS credentials to use a presigned URL?

No - that's the point. The URL carries a temporary signature, so the client uploads directly to S3 with no AWS credentials. The backend's IAM role just needs s3:PutObject.

Can you upload to S3 using a presigned URL?

Yes. Generate the URL server-side with boto3 generate_presigned_url for put_object, then the client PUTs the file directly to S3 with no AWS credentials. Sign put_object (not get_object) or the upload returns 403.

Keep learning

Return the Correct HTTP Status CodeBackend projectRepair Broken API PaginationBackend projectAdd CORS Middleware to an Express APIBackend projectBackend roadmapStep by step to hiredBackend interview questionsSTAR answersAll Backend projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →