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.
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
ClientMethod="get_object"- signs a URL for reading, so a PUT is denied. Uploads needput_object.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
- Sign exactly what the client sends. If the upload sets
Content-Type, include it inParams(and the client must send the same header) or the signature won't match. - The backend's IAM role needs
s3:PutObjecton the bucket/key - the presigned URL only delegates permission the signer already has. - Key per upload - generate a unique object key (e.g.
uploads/<uuid>.jpg) so clients can't overwrite each other.
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
- Signing a put_object URL with a sane ExpiresIn
- Diagnosing a 403 to the method or the expiry
- Generating a unique object key per upload
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
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 →