Upload Generated Images to Amazon S3
Learn how to upload generated images to Amazon S3 with the AWS SDK, CLI, presigned URLs, and multipart uploads—securely and reliably.
Direct answer: Upload a generated image to Amazon S3 as an object in a bucket. On a trusted backend, use an AWS SDK or the AWS CLI with an IAM role that can write to the target bucket. For a browser or mobile client, keep AWS credentials on your server: authenticate the user, choose the object key, create a short-lived presigned PUT URL, and let the client upload the bytes directly to S3. For large or unreliable uploads, use multipart upload so failed parts can be retried independently.
This guide covers server-side uploads, browser uploads without exposing AWS credentials, JavaScript, Python, cURL, the AWS CLI, multipart uploads, checksums, object keys, permissions, troubleshooting, performance, reliability, and cost decisions.
1. Choose the upload design
| Situation | Recommended flow | Where credentials live |
|---|---|---|
| Backend already has the generated bytes | Upload with an AWS SDK or CLI | Backend role or environment credentials |
| Browser or mobile app uploads the image | Backend creates a short-lived presigned PUT URL; client uploads directly to S3 | Only the backend |
| Large file or unreliable network | Multipart upload, preferably through an SDK abstraction | Backend or delegated multipart URLs |
A presigned URL delegates temporary authority for one S3 operation. It does not grant more access than the signer has, and it should be treated like a bearer credential. Scope it to one key and method, keep its lifetime short, and never place long-lived AWS keys in browser JavaScript.
2. Prerequisites
- Create an S3 bucket in the region you intend to use.
- Configure an IAM role or user for the uploader. Grant only the required
s3:PutObjectpermission for the intended bucket prefix. - Choose an object key policy. Include a generated ID or UUID so users cannot overwrite one another accidentally.
- Decide the content type, for example
image/png,image/jpeg, orimage/webp. - Install and configure the AWS SDK or AWS CLI with temporary credentials where possible.
An S3 object consists of a bucket, key, bytes, metadata, and optional tags. Uploading to an existing key replaces that object, so key selection is part of your data-protection design.
3. Upload from a trusted backend with Python
Install the SDK:
pip install boto3
The following program uploads a generated image already stored in memory. The AWS SDK obtains credentials from the standard environment, shared configuration, or an attached role.
import io
import os
import uuid
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3", region_name=os.environ.get("AWS_REGION", "us-east-1"))
bucket = os.environ["S3_BUCKET"]
key = f"generated/{uuid.uuid4()}.png"
# Replace this with the bytes returned by your image-generation code.
image_bytes = b"...generated PNG bytes..."
try:
s3.put_object(
Bucket=bucket,
Key=key,
Body=io.BytesIO(image_bytes),
ContentType="image/png",
CacheControl="public, max-age=31536000, immutable",
)
print(f"s3://{bucket}/{key}")
except ClientError as exc:
print(exc.response.get("Error", {}))
raise
Use a private object by default. If another service needs to display it, return an application-controlled URL or configure an intentional delivery path such as CloudFront. Do not make a bucket public merely to test an upload.
4. Upload with the AWS SDK for JavaScript
Install the v3 client:
npm install @aws-sdk/client-s3
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
const client = new S3Client({ region: process.env.AWS_REGION || "us-east-1" });
const bucket = process.env.S3_BUCKET;
const key = `generated/${randomUUID()}.png`;
const body = await readFile("generated.png");
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: "image/png",
CacheControl: "public, max-age=31536000, immutable"
}));
console.log(`s3://${bucket}/${key}`);
AWS documents @aws-sdk/client-s3 for S3 operations, @aws-sdk/s3-request-presigner for presigned URLs, and @aws-sdk/lib-storage for high-level multipart-capable uploads in Node.js and browsers.
5. Upload with the AWS CLI
aws s3 cp generated.png s3://YOUR_BUCKET/generated/image-001.png \
--content-type image/png \
--cache-control "public,max-age=31536000,immutable" \
--region us-east-1
The CLI uses the credential chain configured on the machine. Prefer an IAM role, workload identity, or short-lived session over a permanent access key in a script.
6. Upload with cURL using a presigned PUT URL
Your backend must generate the URL. The client then sends the image bytes without receiving AWS credentials:
curl --upload-file generated.png \\
-H "Content-Type: image/png" \\
"https://YOUR-PRESIGNED-URL"
When the URL was signed with a content-type header, send the same header during the PUT. A mismatch can produce a signature error.
7. Browser uploads without exposing AWS credentials
The safe pattern is:
- The browser authenticates to your application.
- Your backend validates the user, file type, size, and requested purpose.
- Your backend chooses a unique key such as
users/USER_ID/generated/UUID.png. - Your backend creates a short-lived presigned PUT URL for that exact key.
- The browser uploads the file directly to S3.
- Your backend records the key only after a successful upload, or verifies it with a follow-up HEAD request.
Example presigner endpoint in Node.js:
import express from "express";
import { randomUUID } from "node:crypto";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const app = express();
const s3 = new S3Client({ region: process.env.AWS_REGION || "us-east-1" });
app.get("/upload-url", async (req, res) => {
// Replace this with your real authenticated user ID.
const userId = req.user.id;
const contentType = String(req.query.contentType || "image/png");
const allowed = new Set(["image/png", "image/jpeg", "image/webp"]);
if (!allowed.has(contentType)) return res.status(400).json({ error: "Unsupported image type" });
const extension = contentType.split("/")[1];
const key = `users/${userId}/generated/${randomUUID()}.${extension}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType
});
const url = await getSignedUrl(s3, command, { expiresIn: 300 });
res.json({ url, key, contentType, expiresIn: 300 });
});
app.listen(3000);
Browser code:
async function uploadGeneratedImage(blob) {
const contentType = blob.type || "image/png";
const response = await fetch(`/upload-url?contentType=${encodeURIComponent(contentType)}`);
if (!response.ok) throw new Error(`Could not create upload URL: ${response.status}`);
const { url, key } = await response.json();
const upload = await fetch(url, {
method: "PUT",
headers: { "Content-Type": contentType },
body: blob
});
if (!upload.ok) throw new Error(`S3 upload failed: ${upload.status}`);
return key;
}
Configure S3 CORS for the exact browser origins and methods you need. CORS controls browser access; it does not grant S3 permission by itself.
8. Multipart upload for large generated images
A single PUT supports objects up to 5 GB. AWS documents multipart upload for objects from 5 MB up to 50 TB and recommends considering it for objects 100 MB or larger. Multipart upload splits an object into independently uploaded parts; failed parts can be retried without sending successful parts again.
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";
const upload = new Upload({
client: new S3Client({ region: process.env.AWS_REGION || "us-east-1" }),
params: {
Bucket: process.env.S3_BUCKET,
Key: "generated/large-image.bin",
Body: createReadStream("large-image.bin"),
ContentType: "application/octet-stream"
},
partSize: 10 * 1024 * 1024,
queueSize: 4,
leavePartsOnError: false
});
upload.on("httpUploadProgress", progress => {
console.log(progress.loaded, progress.total);
});
await upload.done();
Production systems should abort abandoned multipart uploads and configure lifecycle cleanup according to current AWS guidance. Do not describe a multipart ETag as the full object’s MD5 hash; use S3 checksum features when you need integrity validation.
9. Integrity checks and metadata
- Set
Content-Typeexplicitly so downstream clients handle the image correctly. - Use a checksum header when your SDK and signing flow support it. AWS Signature Version 4 presigned uploads support additional checksum algorithms when the matching header is included.
- For multipart uploads, use a supplied full-object checksum when you need S3 to reject an assembled object whose checksum does not match.
- Store the generated image’s content hash in your application database if you need deduplication or auditability.
- Do not rely on ETag as a universal MD5 value, especially for multipart objects.
10. Object keys, overwrites, and expiration
An upload to an existing key replaces that object. Use UUIDs or another collision-resistant identifier when each generated image must be retained. If replacement is intentional, enable and understand bucket versioning so recovery behavior is explicit.
A presigned URL can be reused until it expires. Its effective lifetime can be shorter when the signing credentials expire first. Keep URLs short-lived, avoid logging them, and return them only over HTTPS. Treat anyone who obtains the URL as able to perform the signed operation until expiration.
11. Common errors and fixes
| Error | Likely cause | Fix |
|---|---|---|
AccessDenied |
The signer lacks s3:PutObject, the key prefix is blocked, or a bucket policy denies the request. |
Inspect IAM and bucket policy evaluation for the exact bucket and key. |
SignatureDoesNotMatch |
Signed headers, region, method, or query parameters differ from the upload request. | Use the exact method and headers used to create the URL; sign for the bucket’s region. |
ExpiredToken or an expired URL |
The URL or temporary signing credentials expired. | Request a new URL immediately before uploading and keep the upload within its lifetime. |
| Browser CORS error | S3 CORS does not allow the browser origin or PUT method. | Add the exact origin and required headers to the bucket CORS configuration. |
| Image downloads instead of displaying | Content-Type is missing or incorrect. |
Set the correct content type during the signed request and upload. |
| Existing image disappeared | The new upload reused the same key. | Generate unique keys or deliberately use versioning. |
| Large upload fails near the end | Single PUT timeout or transient network failure. | Use multipart upload and retry failed parts. |
| Multipart parts remain billed | An incomplete multipart upload was abandoned. | Abort failed uploads and add lifecycle cleanup for incomplete uploads. |
12. Performance, reliability, and cost considerations
- Transfer path: Presigned uploads send bytes directly from the client to S3, reducing application-server bandwidth and latency.
- Concurrency: Multipart uploads can transfer several parts concurrently. Tune part size and concurrency to available bandwidth and memory.
- Retries: Retry idempotent single-object PUTs with a new request when appropriate. Multipart retries can target only failed parts.
- Caching: Immutable UUID keys allow long cache lifetimes. If a key can be replaced, use conservative cache headers.
- Region: Keep compute, clients, and the bucket geographically appropriate for your latency and data-residency needs.
- Cost: Account for storage, requests, data transfer, and abandoned multipart parts. The correct architecture reduces server transfer but does not make S3 usage free.
- Observability: Record the key, upload result, checksum, byte count, and request identifier without logging presigned URLs.
13. Or skip the browser setup
If the image you need is a rendered website capture, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. You can then stream the response into your S3 upload code. Its API removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
14. Checklist
- Credentials remain on the backend or in a trusted runtime.
- The uploader can write only the intended bucket prefix.
- Keys are unique unless replacement is deliberate.
- Content type and optional checksum are set consistently.
- Presigned URLs are short-lived, HTTPS-only, and treated as bearer credentials.
- Browser CORS allows only required origins and methods.
- Multipart uploads are aborted or cleaned up when abandoned.
- Logs contain object identifiers and results, not presigned URLs.
FAQ
Can I upload directly from a browser with an AWS access key?
Do not embed long-lived AWS credentials in a browser application. Use a backend-generated presigned URL or a carefully designed temporary-credential flow.
Does a presigned URL upload the file for me?
No. It authorizes the client to perform the signed S3 operation. The client still sends the bytes with a PUT request.
When should I use multipart upload?
Use it for large objects, unreliable networks, or when independent part retries materially improve reliability. AWS recommends considering it for objects 100 MB or larger.
Will uploading to the same key create a second image?
No. It replaces the existing object unless bucket versioning preserves older versions.
Is an S3 ETag always an MD5 checksum?
No. Multipart ETags should not automatically be treated as the full object’s MD5 hash. Use checksum features when you need a verified integrity value.


