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:
- Stream Original Bytes from S3: Fetch the object into an in-memory
io.BytesIObuffer without touching disk I/O. - 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. - Multi-Resolution Pyramid Generation: Produce
192w,512w, and1280wvariants using Lanczos resampling (Image.Resampling.LANCZOS) withoptimize=True. - 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%.