Cutting HowdyGo's Vercel Costs by 80% Without Compromising UX or DX

At HowdyGo, we reduced our Vercel spend by over 80% using a self-hosted image optimization solution, without sacrificing performance for users or developers.
Daniel Engelke
October 9, 2024

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.

Lambda function durations

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

Option Cost User Experience Free Tier Setup Time
Continue using Vercel image optimization High: $5 per 1k images beyond free tier Excellent: Seamless integration with Next.js 1k images/month (free plan) or 5k (Pro plan) None: Built-in with Next.js
Not worrying about image optimization None: No optimization costs Poor: Slower load times, worse performance for users Not applicable None: No optimization needed
Using an alternative service (e.g., Cloudflare) Moderate: $0.50 per 1k images, plus storage/delivery Very Good: Comparable performance 5k images/month Moderate: Requires integration with service
Self-hosting image optimization Low: ~$0.02 per 1k images after free tier Very Good: Comparable performance Generous: AWS Free Tier covers ~50k images/month Moderate: Initial setup required with Terraform and AWS

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.

Capture your first product demo today
Free trial, no credit card required
Edit anything in your UI with HTML demos
Hands on onboarding and support
About the Author
Daniel Engelke
Co-Founder

Daniel is a co-founder of HowdyGo and Forbes 30 Under 30 winner. He formerly co-founded and was the CTO of another B2B startup which now has >$20m revenue.

In this article
Double your conversion rate with an interactive demo
Get started for free
Capture your first product demo today
Free trial, no credit card required
Edit anything in your UI with HTML demos
Hands on onboarding and support