
AWS Lambda Thumbnail Creation Demo:
In this tutorial, we will demonstrate how to use AWS Lambda for automatic thumbnail creation. AWS Lambda Thumbnail Creation enables dynamic image processing by leveraging the Serverless Framework along with S3 events. We will also cover function timeouts, memory settings, IAM permissions, plugin configurations for deploying Python dependencies, and environment variables.
Serverless Framework
Imagine a world where you can focus solely on your code without ever worrying about server management. That’s the promise of the serverless framework—a revolutionary approach to building and deploying applications where developers can unleash their creativity without the constraints of traditional infrastructure concerns. With serverless, your code runs in response to events. As a result, it automatically scales to meet demand, while you pay only for the resources you use. It’s the future of application development, where innovation knows no bounds.
Serverless Framework aims to ease the pain of creating, deploying, managing and debugging lambda functions. Moreover, it has CloudFormation support, so your entire stack can be deployed using this framework
Installing Serverless
Install dependencies (node & AWS CLI)
Install Serverless Framework ( sudo npm install -g serverless )
Setting up AWS for the `serverless-admin` user:
Create IAM user with Access Keys
Download credentials on your machine
Setup Serverless to use these credentials ( serverless config credentials --provider aws --key <access key> --secret <secret access key> profile serverless serverless-admin )
Want to learn a Beginner’s Guide to Serverless Computing?

Setup AWS Lambda Thumbnail Creation Generator Code
serverless.yml
service: python-s3-thumbnail
frameworkVersion: '3'
provider:
name: aws
runtime: python3.10
region: us-east-1
profile: serverless-admin
stage: "dev"
timeout: 10
memorySize: 128
environment:
THUMBNAIL_SIZE: "128"
REGION_NAME: ${self:provider.region}
iam:
role:
statements:
- Effect: 'Allow'
Resource: 'arn:aws:s3:::rtvishnu-thumbnails/*'
Action:
- 's3:GetObject'
- 's3:PutObject'
custom:
bucket: rtvishnu-thumbnails
pythonRequirements:
# pythonBin: /opt/python3.8/bin/python
dockerizePip: true
functions:
s3_thumbnail_generator:
handler: handler.s3_thumbnail_generator
events:
- s3:
bucket: ${self:custom.bucket}
event: s3:ObjectCreated:*
rules:
- suffix: .png
#MUST first run: serverless plugin install -n serverless-python-requirements
plugins:
- serverless-python-requirements
handler.py
from datetime import datetime
import boto3
import PIL
from io import BytesIO
from PIL import Image, ImageOps
import os
import uuid
import json
s3 = boto3.client('s3')
size = int(os.environ['THUMBNAIL_SIZE'])
def s3_thumbnail_generator(event, context):
# parse event
print("EVENT:::", event)
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
img_size = event['Records'][0]['s3']['object']['size']
# only create a thumbnail on non thumbnail pictures
if (not key.endswith("_thumbnail.png")):
# get the image
image = get_s3_image(bucket, key)
# resize the image
thumbnail = image_to_thumbnail(image)
# get the new filename
thumbnail_key = new_filename(key)
# upload the file
url = upload_to_s3(bucket, thumbnail_key, thumbnail, img_size)
return url
def get_s3_image(bucket, key):
response = s3.get_object(Bucket=bucket, Key=key)
imagecontent = response['Body'].read()
file = BytesIO(imagecontent)
img = Image.open(file)
return img
def image_to_thumbnail(image):
return ImageOps.fit(image, (size, size), PIL.Image.Resampling.LANCZOS)
def new_filename(key):
key_split = key.rsplit('.', 1)
return key_split[0] + "_thumbnail.png"
def upload_to_s3(bucket, key, image, img_size):
# We're saving the image into a BytesIO object to avoid writing to disk
out_thumbnail = BytesIO() # old way- no longer supported fp.StringIO()
# You MUST specify the file type because there is no file name to discern
# it from
image.save(out_thumbnail, 'PNG')
out_thumbnail.seek(0)
response = s3.put_object(
# NOTE: ACL='public-read' was removed. Buckets created since April 2023
# have ACLs disabled by default, and passing one raises
# AccessControlListNotSupported. See the note below the code.
Body=out_thumbnail,
Bucket=bucket,
ContentType='image/png',
Key=key
)
print(response)
url = '{}/{}/{}'.format(s3.meta.endpoint_url, bucket, key)
return url
We imported PIL and in lambda we need to add a layer dependency. I have used a github repository ( https://github.com/keithrozario/Klayers ) to get the list of ARNs. Get the ARN with respect to the python package version and add layer to lambda function by specifying the ARN

Now deploy the code ( sls deploy )

Check AWS S3 and AWS lambda console, Thumbnail services will be deployed automatically
Buckets are created

Lambda Function Created



Now to test the thumbnail generator service, upload image of type png in rtvishnu-thumbnails bucket and it should trigger lambda function to create thumbnail image (reduce the size of original)


The thumbnail image is resized to a smaller size.
How the handler actually works
The code above is short, but three details in it are doing the real work — and one of them is the difference between a working service and a runaway bill.
The recursion guard is not optional
Look closely at this line:
if (not key.endswith("_thumbnail.png")):The function is triggered by any .png landing in the bucket, and it writes its output back into the same bucket. Without that check, the thumbnail it just saved triggers another invocation, which produces another thumbnail, which triggers another invocation. That is an infinite loop that scales automatically and bills per invocation — the classic S3-trigger footgun, and one of the few ways a hobby project can generate a genuinely alarming AWS bill overnight.
The guard works, but the sturdier pattern is to write thumbnails to a separate bucket. Then the trigger simply cannot fire on your own output, and no string check stands between you and a loop. If you keep one bucket, at minimum add a prefix filter so the trigger only fires on uploads/ while output goes to thumbnails/.
Why the ACL line had to go
The original version passed ACL='public-read' to make thumbnails publicly readable. That will now fail. Since April 2023, new S3 buckets are created with Object Ownership set to “Bucket owner enforced”, which disables ACLs entirely — and a put_object call carrying an ACL raises AccessControlListNotSupported.
If you followed this tutorial and hit that error, that is the cause, not your code. To serve thumbnails publicly now, use a bucket policy or put CloudFront in front of the bucket. Honestly, for most applications a pre-signed URL is the better answer anyway: time-limited access, no public bucket to accidentally leave open.
Working in memory, not on disk
The handler never touches the filesystem — BytesIO keeps the image in memory on the way in and out. That matters because /tmp is scratch space that may or may not survive between invocations, and because skipping disk I/O keeps the function fast. Note the out_thumbnail.seek(0) after saving: without rewinding the buffer, S3 receives zero bytes and you get an empty object with no error.
Tuning memory and timeout for image work
The config above sets timeout: 10 and memorySize: 128. That is fine for small PNGs and will fail on anything substantial — decoding a large image into a Pillow object needs several times the file size in RAM, and a 128 MB function also gets the smallest CPU slice Lambda offers.
| Typical upload | Reasonable memory | Reasonable timeout |
|---|---|---|
| Small PNGs, under ~1 MB | 128–256 MB | 10 s |
| Phone photos, 3–8 MB | 512–1024 MB | 30 s |
| Large or high-resolution images | 1536–2048 MB | 60 s |
Because CPU scales with memory, raising it usually makes the function cheaper rather than more expensive — the job finishes disproportionately faster. A function that takes 8 seconds at 128 MB may take under a second at 1,024 MB, and you are billed for GB-seconds, not for the ceiling you set.
When it does not work
| Error | Cause and fix |
|---|---|
Unable to import module 'handler': No module named 'PIL' | The Pillow layer is missing or its ARN is for the wrong Region or Python version. Layer ARNs are Region-specific — a us-east-1 ARN will not resolve in another Region. |
AccessControlListNotSupported | The ACL problem described above. Remove the ACL argument. |
AccessDenied on read or write | The IAM statement covers arn:aws:s3:::bucket/* (objects). Some operations also need the bucket ARN itself, without /*. |
| Task timed out after 10.00 seconds | Image too large for the current memory and timeout. Raise both per the table above. |
| Function never fires | The event rule has suffix: .png — it is case-sensitive, so .PNG uploads are ignored. |
| Thumbnails keep appearing endlessly | The recursion guard was removed or renamed. Stop the loop by deleting the S3 trigger before anything else. |
Every invocation writes to CloudWatch Logs, and the print("EVENT:::", event) line at the top of the handler exists precisely for this — it shows you the exact event S3 delivered, which is far quicker than guessing at its shape.
Cost, and tearing it down
This stack costs essentially nothing at rest. Lambda’s free tier covers a million requests a month, and S3 charges only for what you store. The two things that can bite are a recursion loop, and CloudWatch Logs retention left on “never expire” — set a retention period of a week or two on the log group and forget about it.
The Serverless Framework deployed everything through CloudFormation, so removal is a single command:
sls removeOne catch: CloudFormation will not delete a bucket that still has objects in it, so empty the bucket first or the stack deletion fails partway through. That is the single most common reason a “deleted” project keeps showing up on a bill.
New to Lambda itself? Start with the fundamentals in our beginner’s guide to serverless computing, which covers execution roles, cold starts and the limits that shape what Lambda can do.
Pingback: Enabling Email Subscriptions With AWS SNS - wordwyzz
It is actually a nice and useful piece of info.
I’m glad that you simply shared this helpful info with us.
Please keep us up to date like this. Thank you for sharing.