Build a Serverless Image Processing Pipeline in 5 Minutes (AWS S3 + Lambda)
Read this article in clean Markdown format for LLMs and AI context.Tired of image uploads choking your SaaS CMS and blowing up your AWS bill? In the next few minutes you’ll get a step‑by‑step, copy‑paste ready solution that moves every resize, thumbnail, and format conversion to a serverless image processing pipeline—so your front‑end stays fast and you only pay for the compute you actually use.
Why image uploads break a SaaS CMS
A single “upload profile picture” feature sounds harmless until dozens of users start sending high‑resolution files each day. Each upload consumes CPU, memory, and bandwidth on the same instances that serve your pages, causing latency spikes and costly scaling events. The core mistake is doing heavy image work on the web‑server instead of offloading it to a managed service.
Set up the serverless image processing pipeline (AWS S3 + Lambda)
- Create an S3 bucket for raw uploads – e.g.,
mycms-uploads. Enable versioning only if you need rollback protection. - Create a second bucket for processed files – e.g.,
mycms-thumbs. You can also use a prefix inside the first bucket if you prefer. - Add a Lambda function: choose Python 3.11 (or your preferred runtime) and attach a role with read/write access to both buckets.
- Configure the trigger – set the function to fire on
ObjectCreated:Putfor the uploads bucket.
Lambda code (copy‑paste)
import os
import boto3
from PIL import Image
import io
s3 = boto3.client('s3')
THUMB_BUCKET = os.environ['THUMB_BUCKET']
SIZE = (300, 300)
def lambda_handler(event, context):
for record in event['Records']:
src_bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
# Download the image
resp = s3.get_object(Bucket=src_bucket, Key=key)
img_data = resp['Body'].read()
img = Image.open(io.BytesIO(img_data))
img.thumbnail(SIZE)
# Save to buffer
out_buffer = io.BytesIO()
img.save(out_buffer, format='JPEG')
out_buffer.seek(0)
# Upload thumbnail
thumb_key = f"thumbs/{key}"
s3.put_object(Bucket=THUMB_BUCKET, Key=thumb_key, Body=out_buffer, ContentType='image/jpeg')
return {'statusCode': 200, 'body': 'done'}
Set the environment variable THUMB_BUCKET to your thumbnail bucket name, save, and you’re done. Every time a file lands in mycms-uploads, Lambda creates a 300 × 300 px thumbnail and writes it to mycms-thumbs. Your CMS can then reference the thumbnail URL directly, eliminating any load on your primary servers.
How the code works
- Event parsing – Lambda receives the S3 event, extracts bucket and object key.
- Image download –
s3.get_objectpulls the raw bytes into memory. - Resize with Pillow –
img.thumbnail(SIZE)maintains aspect ratio while shrinking. - Upload result – The processed buffer is stored back in the thumbnail bucket under a
thumbs/prefix.
Because the function runs only when needed, you get a cost‑effective solution that scales automatically.
Practical tips for production
- Timeout – 10–15 seconds is ample for a single thumbnail; adjust only if you add many sizes.
- Logging – Enable CloudWatch Logs; a silent failure won’t block the upload but will surface in the logs.
- Multiple sizes – Duplicate the
thumb_keyblock with differentSIZEtuples to generate medium, small, or retina versions in the same run. - Security – Restrict the Lambda role to the two specific buckets and enable S3 bucket policies that only allow the Lambda role to write.
Next steps & cost check
- Open the Lambda console, view Invocation count and Duration to see that each thumbnail costs only a few hundredths of a cent.
- Set a billing alarm on Lambda or S3 to catch unexpected spikes.
- Experiment with JPEG quality settings (
img.save(..., quality=75)) to shave storage costs further.
You now have a fast, fully managed pipeline that handles image uploads without any server maintenance. Share this guide with teammates wrestling with the same problem, and feel free to subscribe to [Blog Name] for more bite‑size dev hacks every week.
- →
- →
- →
- →
- →