Cutting HowdyGo's Vercel Costs by 80% Without Compromising UX or DX
Introduction
At HowdyGo, we rely on Vercel to host our app alongside an AWS backend. As our platform enables users to interactively replay recorded web applications, we have to host a lot of image content which needs to load quickly for a great viewing and editing experience.
Initially, we utilized the built-in image optimization provided by Next.js through Vercel, which seemed sufficient at first. However, within just a few months, we hit the limits of what was included in our plan and didn’t feel comfortable with simply paying the additional costs.
This article gives a background on the problems we hit last year, potential solutions that were evaluated, the solution we utilised and the results.
What is Vercel?
Vercel is a cloud hosting platform designed to deploy web applications with a strong emphasis on developer experience (DX). Its user-friendly setup and intuitive features make it incredibly productive for developers, allowing them to get projects up and running quickly compared to other hosting solutions. Vercel’s focus on ease of use and streamlined workflows has made it a popular choice for modern web development.
However, Vercel is also known for its high service markup, which can become expensive at scale. As a result, it’s often necessary to explore alternative options rather than relying solely on Vercel’s default services to avoid unexpected bills.
What is Next Image Optimization?
Next.js offers a range of features such as storage, compute, fonts, and build processes. One key component is its image optimizer, which provides several useful benefits:
- Size Optimization: Automatically serves images in the correct size for each device, using modern formats like WebP and AVIF.
- Visual Stability: Prevents layout shift during image loading.
- Faster Page Loads: Images are loaded only when they enter the viewport, leveraging native browser lazy loading, with optional blur-up placeholders.
- Asset Flexibility: Supports on-demand image resizing, even for images hosted on remote servers.
These features are incredibly useful, ensuring that users only download the images they need, in the right size, which reduces unnecessary load times.
However, Vercel's pricing for image optimization quickly becomes a problem. While the free plan includes 1,000 source image optimizations per month, and the Pro plan ($20/month) covers 5,000 images, each additional 1,000 optimizations costs $5. For example, we optimized around 28,000 images. This would have added $115 to our $20/month hosting fee, basically a near 7x increase in cost.
What are the alternatives?
Here at HowdyGo we considered a few alternatives:
- Continue using the Vercel image optimization (paying the Vercel tax)
- Not worrying about image optimization
- Using an alternative service
- Self hosting your own image optimization
Continue using the Vercel image optimization (paying the Vercel tax)
This is the simplest option - not changing anything and just absorbing the costs. However, this would have resulted in some growing Vercel bills, given on our application’s usage as we scaled. Given our relatively low price compared to our competitors, we did not this solution was viable. We didn’t want to spend more on hosting than necessary, especially when there were cost-effective alternatives available.
Not worrying about image optimization
Another simple option is to skip image optimization altogether. According to Vercel’s documentation, this can be done by passing an unoptimized
flag to the Image component.
import Image from 'next/image';
export default function ImageExample() {
return (
<Image
unoptimized
src="<https://unsplash.com/photos/MpL4w1vb798>"
alt="Picture of a triangle"
width={500}
height={500}
/>
);
}
This does mean load times and data transferred are increased which may not matter or be required for specific uses case. However, in our case, transferring large volumes of images would significantly slow down load times especially in our editor so this wasn’t a practical solution.
Alternative Services
Another option is Cloudflare’s image optimizer service, which integrates with their storage solution. Cloudflare is a global network provider offering fast, reliable solutions for web performance, including content delivery (CDN), storage, and compute.
Cloudflare’s image optimization service is also highly competitive in pricing:
MetricPricingImages TransformedFirst 5,000 unique transformations included + $0.50 / 1,000 unique transformations / monthImages Stored$5 / 100,000 images stored / monthImages Delivered$1 / 100,000 images delivered / month
For the 28,000 images we optimized last month, Cloudflare would have cost us $11.50 for transformations, plus a small additional fee for storage and delivery.
Self hosting your own image optimization
In the self hosting space there are plenty of options available, one option we considered was a Terraform Next.js module by milliVolt infrastructure, which utilises AWS services like S3, Cloudfront and Lambda to provide image optimization with any easy to use terraform module.
At HowdyGo, we already use AWS S3 for asset storage and Terraform for configuring backend services. Extending our existing setup to include a Next.js image optimizer function in our S3 and CloudFront terraform configuration seemed like any easy change to make.
A key advantage of this solution beyond using our existing infrastructure was the ability to utilize the AWS Free Tier. We estimated that the free tier would cover our first 50,000 images, with costs after that around $0.02 per 1,000 images. Comparatively this is 25 times cheaper than Cloudflare and 250 times cheaper than Vercel!
The self hosted setup
Infrastructure Changes
Deploying the Next.js image optimizer module to our AWS account was straightforward. We simply modified our existing Terraform configuration for S3 and CloudFront to integrate the image optimizer. The code is provided below:
module "next_image_optimizer" {
source = "milliHQ/next-js-image-optimization/aws"
# Enable or disable the optimizer based on a variable
count = var.deploy_image_optimizer == true ? 1 : 0
# Prevent creation of the internal CloudFront distribution
cloudfront_create_distribution = false
# Specify a unique deployment name
deployment_name = "${var.env_short}-${var.name}-next-image-optimizer"
# Configure cloudfront custom domain
cloudfront_aliases = ["${var.cdn_domain_name}"]
cloudfront_acm_certificate_arn = aws_acm_certificate.cert.id
# Configure source bucket to pull data from s3 directly
source_bucket_id = aws_s3_bucket.default_bucket.id
# Allow lambda function to access s3 bucket
lambda_memory_size = 3008
lambda_attach_policy_json = true
lambda_policy_json = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"s3:GetObject",
"s3:ListBucket"
],
"Resource" : [
"${aws_s3_bucket.default_bucket.arn}/*"
]
}
]
})
}
As the S3 bucket already contained all the images we were hosting we didn’t need to migrate our content.
App side changes
Next.js supports image loaders, which can be configured either globally or per component.
For our needs, we created a CustomNextImage
component to display user-generated content.
import React from "react";
import Image, { ImageProps } from "next/image";
import { customLoader } from "../../lib/image-loader";
const CustomNextImage: React.FC<ImageProps> = ({ src, alt, ...rest }) => {
return (
<Image
src={src}
alt={alt}
{...rest}
unoptimized={String(src).endsWith(".svg")}
loader={customLoader}
/>
);
};
export default CustomNextImage;
The CustomNextImage
component uses a custom loader that checks if the URL starts with a specific path; otherwise, it falls back to the default Next.js image loader on Vercel.
export function customLoader({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number | undefined;
}) {
if (src.startsWith(cacheUrl)) {
// Use self host next-image for cacheUrl
return `${cacheUrl}/_next/image?url=${src.slice(
cacheUrl.length,
)}&w=${width}&q=${quality || 75}`;
} else {
// Use usual next image loader
return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${
quality || 75
}`;
}
}
Results
In terms of compute costs, we remained well within the AWS Lambda Free Tier, so all requests were free. The average duration to execute the function was 365ms, with a range from 1.65ms to 1756ms. Overall performance was excellent, with no noticeable impact on the end user.
From a developer experience standpoint, once the component was deployed, there was no impact on productivity. The process was seamless, and our team could continue working without any interruptions or additional overhead.
Comparison Table
Conclusion
By migrating away from Vercel’s image optimization service to a self-hosted solution using AWS and Terraform, we reduced our overall hosting costs by over 80%, all while maintaining excellent performance and a seamless developer experience. While Vercel’s tools are convenient and developer-friendly, the expensive costs at scale made us seek out more cost effective alternatives. Cloudflare’s image optimizer service is another viable, cost-effective option, but for us, the self-hosted solution fit best at the time.
The AWS Lambda-based image optimization kept us well within the free tier, with performance that remained fast and reliable for users. Just as important, the shift had no impact on developer productivity - once deployed, the component ran smoothly without any additional overhead. This was critical in maintaining HowdyGo’s developer experience while optimizing costs.
As your web application grows, evaluating the trade-offs between pricing and performance with services like Vercel is essential. At HowdyGo, this evaluation led to a cost-efficient and scalable image optimization solution that integrated smoothly with our existing infrastructure, without sacrificing user or developer experience.