Home / Blog / Cost Effective Image Optimization Solution With AWS

Cost Effective Image Optimization Solution With AWS

7 min read.May 25, 2025

Cost Effective Image Optimization Solution With AWS

Images are the heaviest component in web applications, serving images optimistically with proper size, format is often required to make the site looks technically rich.

From the next lines we will be discovering how we can develop our own Image Optimization Solution like Cloudinary or Imagekit by using AWS Resources.

Lets understand what's the goal?

We need to develop something where we can upload images and when we try to get the images by its url -

  1. It should serve the images faster.

  2. It should support height and width in query parameters.

  3. Its should support format(e.g. webp, png, jpeg) and quality.

  4. If a image doesn't exist with a combination of parameters, the Image should auto generated.

  5. Once a images generated with set of parameters it should not regenerate in each request.

Aws Resources we are going to use

  • S3 Bucket

  • Cloudfront

  • Cloudfront Edge functions

  • Lambda function

High Level System Design

We will be creating two S3 buckets on for storing original images uploaded by the user and second one for storing the transformed images.

We will create a cloudfront distribution and setup transform image bucket as origin  so every image will be serve by transformed bucket and a lambda function as fallback.

We will also attach a Cloudfront edge function with cloudfront which will normalize the query parameter requested by the user and parse the queries as path.

for example : -

?height=200&width=300&format=webp parse to /height=200,width=300,format=webp

if no query are passed it will normalize  to /original

If the image not found in the transformed bucket cloudfront will fallback to lambda function which actually retrieve the image from Original S3 bucket and transform it based of requested parameters and store it to Transformed S3 bucket and redirect user to the same url so from next request the image exist in the transform bucket and served directly.

Aws image optimization high level system design
Aws image optimization high level system design

Normalize URL with Edge Function

function assertSearchParams(event) {

    var querystring = event.request.querystring;

    if (!querystring || !Object.keys(querystring).length) return {};

    var transformOptions = {};

    Object.entries(querystring).forEach(

        function (entry) {

            var key = entry[0];
            var value = Array.isArray(entry[1].value) ? entry[1].value[entry[1].value.length - 1] : entry[1].value

            if ((key === 'height' || key === 'width') && !isNaN(value)) {
                transformOptions[key] = parseInt(value);
            }

            if (key === 'quality' && !isNaN(value)) {

                if ((parseInt(value) >= 0) && (parseInt(value) <= 100)) {
                    transformOptions[key] = parseInt(value);
                }
            }

            if (key === 'format' && ['jpeg', 'gif', 'webp', 'png', 'avif'].includes(value)) {
                transformOptions[key] = value
            }
        }
    )

    return transformOptions;
}


function handler(event) {

    var uri = event.request.uri;
    var searchParams = assertSearchParams(event);

    if (Object.keys(searchParams).length > 0) {

        var tranformationStrings = [];

        if (searchParams.height) {
            tranformationStrings.push(`height=${searchParams.height}`);
        }

        if (searchParams.width) {
            tranformationStrings.push(`width=${searchParams.width}`);
        }

        if (searchParams.format) {
            tranformationStrings.push(`format=${searchParams.format}`);
            tranformationStrings.push(`quality=${searchParams.quality || 100}`);
        }

        event.request.uri = uri + '/' + tranformationStrings.join(',');

    } else {

        event.request.uri = uri + '/original';
    }

    event.request['querystring'] = {};

    return event.request;
}

How Aws Lambda Function Handle Fallbacks

import Sharp from 'sharp';
import mime from 'mime-types';
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const s3Client = new S3Client();

const parsePath = (event) => {

    const __chunks = event.requestContext.http.path.split('/');

    if (__chunks[__chunks.length - 1] === 'original') {
        __chunks.pop();
        return {
            path: __chunks.join('/'),
            options: {}
        }
    }

    const transformOptions = {};

    __chunks[__chunks.length - 1].split(',').forEach(
        (chunk) => {
            const parts = chunk?.split('=');
            transformOptions[parts[0]] = parts[1];
        }
    )

    if (Object.keys(transformOptions).length) __chunks.pop();

    Object.entries(transformOptions).map(

        ([key, value]) => {

            if (key === 'height' || key === 'width') {

                if (!transformOptions['resize']) transformOptions['resize'] = {}

                transformOptions['resize'][key] = parseInt(value);

                delete transformOptions[key];
            }

            if (key === 'quality') {
                transformOptions[key] = parseInt(value);
            }

            if (key === 'format') {
                transformOptions[key] = value;
            }
        }
    )

    return {
        path: __chunks.join('/'),
        options: transformOptions,
    }
}

const handler = async (event) => {

    if (event?.requestContext?.http?.method != 'GET') return { statusCode: 400, body: 'Only GET method is supported' };

    const { path, options } = parsePath(event);

    const prefix = event.requestContext.http.path.split('/').pop();

    try {
        const originalObject = await s3Client.send(

            new GetObjectCommand(
                {
                    Key: path.substring(1),
                    Bucket: process.env.S3_ORIGINAL_IMAGE_BUCKET_NAME
                }
            )
        );

        const objectBuffer = await originalObject.Body.transformToByteArray();

        if (!originalObject.ContentType.startsWith('image/') || originalObject.ContentType.includes('icon') || originalObject.ContentType.includes('svg')) {

            await s3Client.send(

                new PutObjectCommand(
                    {
                        Body: objectBuffer,
                        Bucket: process.env.S3_TRANSFORMED_IMAGE_BUCKET_NAME,
                        Key: event.requestContext.http.path.substring(1),
                        ContentType: originalObject.ContentType,
                        CacheControl: process.env.S3_TRANSFORMED_IMAGE_CACHE_TTL,
                    }
                )
            );

            return {
                statusCode: 302,
                headers: {
                    'Location': prefix.includes('original') ? path : path + '?' + prefix.replace(/,/g, "&"),
                    'Cache-Control': 'private,no-store'
                }
            };
        }

        let transformedObject = Sharp(objectBuffer, { failOn: 'none', animated: true });

        if (options?.resize) transformedObject = transformedObject.resize(options.resize);

        if (options?.format) transformedObject = transformedObject.toFormat(
            options.format,
            {
                quality: options.quality,
            }
        );

        transformedObject = await transformedObject.toBuffer();

        await s3Client.send(

            new PutObjectCommand(
                {
                    Body: transformedObject,
                    Bucket: process.env.S3_TRANSFORMED_IMAGE_BUCKET_NAME,
                    Key: event.requestContext.http.path.substring(1),
                    ContentType: mime.lookup(options?.format) || originalObject.ContentType,
                    CacheControl: process.env.S3_TRANSFORMED_IMAGE_CACHE_TTL,
                }
            )
        );

        return {
            statusCode: 302,
            headers: {
                'Location': prefix.includes('original') ? path : path + '?' + prefix.replace(/,/g, "&"),
                'Cache-Control': 'private,no-store'
            }
        };

    } catch (error) {

        return { statusCode: 404, body: 'Not Found' }
    }
}

export { handler }

Common Use Cases

One of the most common use cases for image optimization is to automatically adjust image formats based on the user’s browser and device, while also enabling the front end to dynamically resize images. Modern web frameworks like Next.js offer responsive image components that automatically select the optimal image size based on the device’s viewport. 

Should we Create all these manually?

Umm! no right, so we need some way to automate the entire process, for that we can use Aws CloudFormation , where we can write code to create this infrastructure. Here is a simple solution to create this infrastructure with Aws Cloudformation and NodeJs sdk.

Published InSystem Design

System design with real-world examples. Explore architecture patterns, scalability, databases, microservices, and more to build robust systems.