Serving and receiving files through your own servers is slow and expensive; presigned URLs hand the transfer to S3 securely.
A presigned URL is a normal S3 URL with a signature in its query string, created locally with your credentials. Anyone holding it can perform that one operation until it expires, without AWS credentials of their own.
- Downloads:
s3.generate_presigned_url("get_object", Params={"Bucket": b, "Key": k}, ExpiresIn=900). Add"ResponseContentDisposition": 'attachment; filename="report.pdf"'to force a download name. - Uploads:
generate_presigned_url("put_object", Params={..., "ContentType": "image/png"}); the client must then send exactly the signed headers. For browser form uploads with size and type limits,generate_presigned_post(Bucket, Key, Conditions=[["content-length-range", 0, 10_000_000]], ExpiresIn=600)returns a URL plus form fields.
Things to know:
- Generating a URL makes no network call and doesn't check that the object exists or that you have permission; those are checked when the URL is used, against the signer's permissions at that time.
- The URL can't outlive the credentials that signed it: URLs signed with temporary role credentials (Lambda, ECS, SSO) stop working when the session expires, even if
ExpiresInis longer. The maximum is 7 days. - Create the client with the bucket's region and
Config(signature_version="s3v4"). botocore still signs S3 presigned URLs with the legacy SigV2 scheme unless told otherwise, and SigV4 is what new regions and security policies require.
Treat presigned URLs like passwords: short expiry, HTTPS only, and don't log them.
Example
import boto3, requests
from botocore.config import Config
s3 = boto3.client("s3", region_name="us-east-1", config=Config(signature_version="s3v4"))
s3.create_bucket(Bucket="demo-share-2026")
s3.put_object(Bucket="demo-share-2026", Key="reports/q1.pdf", Body=b"%PDF-1.7 ...")
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "demo-share-2026", "Key": "reports/q1.pdf",
"ResponseContentDisposition": 'attachment; filename="q1.pdf"'},
ExpiresIn=900) # 15 minutes
print(url.split("?")[0]) # https://demo-share-2026.s3.amazonaws.com/reports/q1.pdf
print("X-Amz-Signature=" in url) # True: SigV4
post = s3.generate_presigned_post(
"demo-share-2026", "uploads/avatar.png",
Conditions=[["content-length-range", 0, 2_000_000]], ExpiresIn=600)
r = requests.post(post["url"], data=post["fields"],
files={"file": ("avatar.png", b"\x89PNG...")})
print(r.status_code) # 204Remember
- generate_presigned_url signs one operation locally, no network call
- Set signature_version='s3v4'; use generate_presigned_post for browser uploads
- URLs can't outlive the signing credentials; max 7 days
- Keep expiry short and treat URLs as secrets
Watch out for
- Presigning with Lambda's temporary credentials for 7 days and seeing links break after a few hours.