AWS S3 + Lambda: Serverless Image Processing Pipeline
Tech Setup1 min read
TS
Published July 30, 2026 · Editorial policy

Architecture Overview
When a user uploads an image to S3, a Lambda function automatically triggers, processes the image (resize, compress, convert to WebP), and saves the result back to S3.
Upload → S3 (original) → Lambda trigger → Process → S3 (processed)
Prerequisites
- AWS CLI configured (
aws configure) - Node.js 18+ installed
- Basic understanding of S3 and Lambda
Step 1: Create S3 Bucket
aws s3 mb s3://my-image-uploads-$(date +%s) --region us-east-1
Or via console:
- Go to S3 → Create bucket
- Name:
my-image-uploads - Region:
us-east-1 - Block all public access: ON
- Enable bucket versioning: OFF
Step 2: Create Lambda Function
mkdir image-processor && cd image-processor
npm init -y
npm install sharp @aws-sdk/client-s3
index.mjs
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.PROCESSED_BUCKET;
const SIZES = [
{ suffix: "thumb", width: 150, height: 150 },
{ suffix: "medium", width: 800, height: 600 },
{ suffix: "large", width: 1920, height: 1080 },
];
export const handler = async (event) => {
const srcKey = decodeURIComponent(
event.Records[0].s3.object.key.replace(/\+/g, " ")
);
console.log(`Processing: ${srcKey}`);
// Get original image
const { Body } = await s3.send(
new GetObjectCommand({
Bucket: event.Records[0].s3.bucket.name,
Key: srcKey,
})
);
const buffer = Buffer.from(await Body.transformToByteArray());
const baseName = srcKey.replace(/\.[^.]+$/, "");
// Process each size
for (const size of SIZES) {
const processed = await sharp(buffer)
.resize(size.width, size.height, { fit: "cover" })
.webp({ quality: 80 })
.toBuffer();
const destKey = `${baseName}-${size.suffix}.webp`;
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: destKey,
Body: processed,
ContentType: "image/webp",
})
);
console.log(`Created: ${destKey} (${processed.length} bytes)`);
}
return { statusCode: 200, body: "Processed" };
};
Step 3: IAM Policy
Create lambda-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::my-image-uploads/*",
"arn:aws:s3:::my-processed-images/*"
]
}
]
}
Step 4: Deploy
# Create deployment package
zip -r function.zip index.mjs node_modules/
# Create Lambda function
aws lambda create-function \
--function-name image-processor \
--runtime nodejs20.x \
--role arn:aws:iam::YOUR_ACCOUNT:role/lambda-s3-role \
--handler index.handler \
--zip-file fileb://function.zip \
--timeout 30 \
--memory-size 512 \
--environment "Variables={PROCESSED_BUCKET=my-processed-images}"
Step 5: Add S3 Trigger
aws lambda add-permission \
--function-name image-processor \
--principal s3.amazonaws.com \
--action lambda:InvokeFunction \
--source-arn arn:aws:s3:::my-image-uploads \
--source-account YOUR_ACCOUNT_ID
Then add the notification in S3 bucket properties:
{
"LambdaFunctionConfigurations": [
{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:ACCOUNT:function:image-processor",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"Filters": [
{ "Name": "prefix", "Value": "uploads/" },
{ "Name": "suffix", "Value": ".jpg" }
]
}
}
}
]
}
Step 6: Test
aws s3 cp test-image.jpg s3://my-image-uploads/uploads/test-image.jpg
# Check Lambda logs
aws logs tail /aws/lambda/image-processor --follow
Cost Estimate
- S3 storage: ~$0.023/GB/month
- Lambda: 1M free requests/month, then $0.20/1M
- For 1000 images/day: roughly $0.50/month
Production Tips
- Set Lambda memory to 1024MB+ for sharp to run fast
- Add error handling — dead letter queue for failed processing
- Use S3 Lifecycle policies to delete originals after processing
- Add CloudWatch alarms for Lambda errors
- Consider SQS for decoupling if processing is heavy


