Home / Blog / Cost Effective Image Optimization Solution With AWS
Cost Effective Image Optimization Solution With AWS
7 min read.May 25, 2025

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 -
It should serve the images faster.
It should support height and width in query parameters.
Its should support format(e.g.
webp
,png
,jpeg
) and quality.If a image doesn't exist with a combination of parameters, the Image should auto generated.
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.

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.