S3 presigned URLs provide temporary, secure access to private S3 objects. Share download links or allow uploads without exposing your AWS credentials or making buckets public.
Key Takeaways
- 1Presigned URLs grant temporary access to private S3 objects
- 2URLs expire after a configurable time (default 15 minutes, max 7 days)
- 3Support GET (download), PUT (upload), and other S3 operations
- 4Generated server-side using AWS SDK—never expose credentials client-side
- 5Include signature, expiration, and permissions in query parameters
Presigned URL Structure
Every presigned URL contains several query parameters that AWS uses to verify the request. These parameters include the signing algorithm, your credentials scope, a timestamp, and the cryptographic signature itself. Understanding this structure helps you debug access issues and validate URLs before sharing them.
https://bucket-name.s3.amazonaws.com/path/to/file.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...&X-Amz-Date=20260115T120000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123...The table below explains each query parameter you will see in a presigned URL. When troubleshooting 403 errors, check that the timestamp and expiration values are valid for the current time.
| Parameter | Description |
|---|---|
X-Amz-Algorithm | Signing algorithm (AWS4-HMAC-SHA256) |
X-Amz-Credential | Access key and scope |
X-Amz-Date | Request timestamp |
X-Amz-Expires | Seconds until expiration |
X-Amz-SignedHeaders | Headers included in signature |
X-Amz-Signature | Request signature |
Now that you understand the URL structure, let's look at how to generate presigned URLs for common operations using the AWS SDK.
Generate Download URL
Download URLs are the most common use case for presigned URLs. You generate a URL on your server and share it with users who need temporary access to a file. This keeps your bucket private while allowing controlled access to specific objects.
// Node.js with AWS SDK v3
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client({ region: 'us-east-1' });
async function generateDownloadUrl(bucket, key, expiresIn = 3600) {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
const url = await getSignedUrl(s3Client, command, { expiresIn });
return url;
}
// Usage
const downloadUrl = await generateDownloadUrl(
'my-bucket',
'documents/report.pdf',
3600 // 1 hour
);This code creates a presigned URL that allows downloading the specified file for one hour. The GetObjectCommand specifies the bucket and key, while getSignedUrl adds the authentication parameters.
Generate Upload URL
Upload URLs enable your users to upload files directly to S3 from their browser. This offloads the file transfer from your server and lets users upload large files without passing through your infrastructure. You specify the content type when generating the URL to prevent users from uploading unexpected file types.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
async function generateUploadUrl(bucket, key, contentType, expiresIn = 3600) {
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(s3Client, command, { expiresIn });
return url;
}
// Generate upload URL
const uploadUrl = await generateUploadUrl(
'my-bucket',
'uploads/user-photo.jpg',
'image/jpeg',
900 // 15 minutes
);
// Client uploads directly to S3
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: fileBlob
});The upload flow has two steps: first, your server generates the presigned URL with PutObjectCommand, then the client uploads directly to S3 using a standard PUT request. Notice that the content type must match between the URL generation and the actual upload.
Python Example
If you are working with Python, boto3 provides similar functionality. The example below shows how to generate both download and upload URLs using the Python SDK.
import boto3
from botocore.config import Config
s3_client = boto3.client(
's3',
region_name='us-east-1',
config=Config(signature_version='s3v4')
)
def generate_presigned_url(bucket, key, expiration=3600, operation='get_object'):
"""Generate a presigned URL for S3 object access."""
try:
url = s3_client.generate_presigned_url(
ClientMethod=operation,
Params={'Bucket': bucket, 'Key': key},
ExpiresIn=expiration
)
return url
except Exception as e:
print(f"Error generating presigned URL: {e}")
return None
# Download URL
download_url = generate_presigned_url('my-bucket', 'file.pdf')
# Upload URL
upload_url = generate_presigned_url(
'my-bucket',
'uploads/new-file.pdf',
expiration=900,
operation='put_object'
)The Python SDK uses generate_presigned_url with a ClientMethod parameter to specify the operation. For signature version 4 compatibility, explicitly set the signature version in the config.
With the basics covered, let's see how to integrate presigned URL generation into a real API endpoint that your frontend can call.
API Endpoint Example
In production, you will typically create an API endpoint that generates presigned URLs on demand. The endpoint receives the file metadata from your client, generates a unique key to prevent overwrites, and returns the presigned URL along with the S3 key for storage in your database.
// Express.js API endpoint
import express from 'express';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
const s3Client = new S3Client({ region: process.env.AWS_REGION });
router.post('/get-upload-url', async (req, res) => {
const { fileName, contentType } = req.body;
// Generate unique key to prevent overwrites
const key = `uploads/${uuidv4()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
});
try {
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 900 });
res.json({
uploadUrl,
key,
expiresIn: 900
});
} catch (error) {
res.status(500).json({ error: 'Failed to generate upload URL' });
}
});This Express endpoint demonstrates the complete server-side flow. The UUID prefix ensures each upload gets a unique key, preventing users from accidentally overwriting each other's files.
Client-Side Upload
On the frontend, your upload flow is a two-step process: request a presigned URL from your API, then upload directly to S3. This pattern keeps your server out of the data transfer path, reducing bandwidth costs and improving upload speeds.
async function uploadFile(file) {
// 1. Get presigned URL from your API
const response = await fetch('/api/get-upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
contentType: file.type
})
});
const { uploadUrl, key } = await response.json();
// 2. Upload directly to S3
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
if (!uploadResponse.ok) {
throw new Error('Upload failed');
}
// 3. Return the S3 key for storage in your database
return key;
}The client-side code first fetches the presigned URL from your API, then uses fetch with the PUT method to upload directly to S3. After a successful upload, you should store the S3 key in your database to reference the file later.
Expiration Guidelines
Choosing the right expiration time balances security with user experience. Shorter expirations are more secure but may frustrate users if links expire before they can use them. The table below provides recommendations for common scenarios.
| Use Case | Recommended Expiration |
|---|---|
| Direct download link | 1-24 hours |
| Upload from browser | 5-15 minutes |
| Email download link | 1-7 days |
| API file access | 15 minutes |
Security is critical when working with presigned URLs. A leaked URL grants access to your private data until it expires. Follow these best practices to minimize risk.
Security Best Practices
Generate URLs server-side only
Never expose AWS credentials in client-side code.
Use minimal expiration times
15 minutes for uploads, hours for downloads—never longer than needed.
Validate file types and sizes
Check content type and file size before generating upload URLs.
Use unique keys for uploads
Generate UUIDs to prevent users from overwriting each other's files.
Try the URL Builder
Use our S3 Presigned URL template to explore the URL structure. You can break down the query parameters to understand how each component contributes to the signed request.