AWS Lambda: High-Speed Serverless Image Resizer with Python Pillow & S3

Generating responsive image thumbnails on the fly inside a monolithic web server quickly starves CPU workers and inflates memory usage. By offloading image resizing and WebP/AVIF transcoding to an event-driven AWS Lambda function written in Python with Pillow (SIMD), you achieve infinite burst scalability at near-zero idle cost.

1. Architecture Overview: S3 Event Notifications to Lambda

When a user uploads an original high-resolution photo to an ingress S3 bucket (via pre-signed POST URLs), Amazon S3 emits an s3:ObjectCreated:* event that invokes our Python Lambda function asynchronously:

  1. Stream Original Bytes from S3: Fetch the object into an in-memory io.BytesIO buffer without touching disk I/O.
  2. EXIF Orientation & RGBA Normalization: Apply ImageOps.exif_transpose(img) so smartphone portrait photos never render sideways, and composite alpha channels cleanly before JPEG/WebP conversion.
  3. Multi-Resolution Pyramid Generation: Produce 192w, 512w, and 1280w variants using Lanczos resampling (Image.Resampling.LANCZOS) with optimize=True.
  4. Immutable Cache-Control Headers: Write the resized variants to the CDN-backed output bucket with Cache-Control: public, max-age=31536000, immutable.

2. Production Python Lambda Handler

import io
import os
import urllib.parse
import boto3
from PIL import Image, ImageOps

s3 = boto3.client("s3")
DEST_BUCKET = os.environ["DEST_BUCKET"]
SIZES = [(192, 192), (512, 512), (1280, 1280)]

def lambda_handler(event, context):
    for record in event.get("Records", []):
        src_bucket = record["s3"]["bucket"]["name"]
        src_key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])

        response = s3.get_object(Bucket=src_bucket, Key=src_key)
        raw_bytes = response["Body"].read()

        with Image.open(io.BytesIO(raw_bytes)) as img:
            img = ImageOps.exif_transpose(img)
            if img.mode in ("RGBA", "P"):
                img = img.convert("RGB")

            for width, height in SIZES:
                copy_img = img.copy()
                copy_img.thumbnail((width, height), Image.Resampling.LANCZOS)
                out_buffer = io.BytesIO()
                copy_img.save(out_buffer, format="WEBP", quality=82, method=6)
                out_buffer.seek(0)

                dest_key = f"{width}x{height}/{ os.path.splitext(src_key)[0] }.webp"
                s3.put_object(
                    Bucket=DEST_BUCKET,
                    Key=dest_key,
                    Body=out_buffer,
                    ContentType="image/webp",
                    CacheControl="public, max-age=31536000, immutable",
                )
    return {"statusCode": 200}

3. Packaging Pillow with AWS Lambda Layers & ARM64 Graviton2

Always compile or pull manylinux2014_aarch64 wheels and run your image-processing Lambda on ARM64 (Graviton2) with 1,769 MB of memory (which allocates a full vCPU core), cutting execution duration and cost by up to 34%.


About the Author — Hire Freelance Programmer Aldian Fazrihady

Aldian Fazrihady is a Top-Rated Senior Freelance Programmer, Freelance Full Stack Developer, and AI Application Engineer with 15+ years of production software engineering experience and Google Cloud Professional Cloud Architect & Data Engineer certifications. He specializes in building custom LLM & autonomous AI agents, RAG pipelines, Next.js/React PWAs, Python (Django/FastAPI), Go microservices, and serverless cloud infrastructure on Google Cloud Run and AWS.

Hire Aldian Fazrihady on Upwork | View Live AI Portfolio | Portfolio Directory | Send a Direct Message

Related Engineering Articles

Freelance Programmer Specializations & Hiring Guides