Home Blog About Contact AWS Artificial Intelligence
AWS Beginner

Amazon S3 Trigger For AWS Lambda Thumbnail Creation

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…

WWWordWyzz ·Published May 6, 2024 ·Updated Aug 17, 2026 ·8 min read ·181 views
Overview diagram of the thumbnail service: an image uploaded to S3 triggers a Lambda function that writes a resized thumbnail.

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.

https://www.serverless.com

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?

Configuring Serverless Framework AWS credentials with an access key and secret access key.

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

Adding a Lambda layer by specifying the ARN for the required Python package version.

Now deploy the code ( sls deploy )

Deploying the service with the sls deploy command.

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

Buckets are created

S3 console showing the buckets created automatically by the deployment.

Lambda Function Created

Lambda console showing the thumbnail function created by the deployment.
Lambda function configuration with its trigger, runtime and handler settings.
S3 event trigger wired to the Lambda function for object-created events.

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)

Uploading a PNG image to the thumbnails bucket to trigger the Lambda function.
Generated thumbnail written to the destination bucket at a reduced size.

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 uploadReasonable memoryReasonable timeout
Small PNGs, under ~1 MB128–256 MB10 s
Phone photos, 3–8 MB512–1024 MB30 s
Large or high-resolution images1536–2048 MB60 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

ErrorCause 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.
AccessControlListNotSupportedThe ACL problem described above. Remove the ACL argument.
AccessDenied on read or writeThe IAM statement covers arn:aws:s3:::bucket/* (objects). Some operations also need the bucket ARN itself, without /*.
Task timed out after 10.00 secondsImage too large for the current memory and timeout. Raise both per the table above.
Function never firesThe event rule has suffix: .png — it is case-sensitive, so .PNG uploads are ignored.
Thumbnails keep appearing endlesslyThe 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 remove

One 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.

WW
Written by
WordWyzz
Cloud & AI Engineering

Hands-on guides to building production-ready cloud and AI systems on AWS — written by Raviteja Vishnubhotla, an AWS practitioner, for practitioners.

This Post Has 2 Comments

  1. visit article

    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.

Leave a Reply