DevOps

AWS S3 + Lambda: Serverless Image Processing Pipeline

Tech Setup1 min read
TS

Tech Setup

Published July 30, 2026 · Editorial policy

AWS S3 + Lambda: Serverless Image Processing Pipeline

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:

  1. Go to S3 → Create bucket
  2. Name: my-image-uploads
  3. Region: us-east-1
  4. Block all public access: ON
  5. 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

  1. Set Lambda memory to 1024MB+ for sharp to run fast
  2. Add error handling — dead letter queue for failed processing
  3. Use S3 Lifecycle policies to delete originals after processing
  4. Add CloudWatch alarms for Lambda errors
  5. Consider SQS for decoupling if processing is heavy