How to reduce your Vercel bill
A couple of weeks ago, I got an email from Vercel: 50% of the credits included in my Pro plan were already gone. Only 2 days into my new billing cycle. For context, Iโm working on a pet-project: Framecount, a movie-trac
A couple of weeks ago, I got an email from Vercel: 50% of the credits included in my Pro plan were already gone. Only 2 days into my new billing cycle.
For context, Iโm working on a pet-project: Framecount, a movie-tracking app, which had been running for almost a year with ~200โ300 monthly users and zero issues. However, I recently published a dynamic sitemap with more than 15,000 entries, and a few weeks later, traffic went up like crazy, and that made the bill explode also.
Over the next two weeks, I went through a painful trial-and-error process trying to reduce costs. Some optimizations barely helped, but one in particular reduced my daily spend by exactly 92,37% almost instantly.
This post is a breakdown of some of the things I ran into when optimizing Vercel costs, explained as simply as possible, because a week ago, I didnโt understand any of this either.
My journey to reduce Vercel costs
How Vercel bills you (a mental model)
Most Vercel bills come from 4 buckets:
| Category | What it means | Typical causes |
|---|---|---|
| Image Transformations | On-demand image resizing & encoding | Abusing next/image + many sizes in config |
| Fast Data Transfer | Static assets served from CDN โ user traffic | Large pages + a lot of static assets |
| Fast Origin Transfer | Functions, ISR, Middleware | Server fetches and unwanted side-effects |
| Edge Requests | Cached & uncached requests | Big amount or uncached requests (Bots, crawlers) |
Vercel Firewall: The Biggest Win
After trying multiple optimizations just to save a few cents per day, I realized the real cost was caused by another thing. A couple of weeks after publishing a huge sitemap, my site started receiving tens of thousands of requests per day. I didn't know at that moment, but most of them were automated, many hitting expensive routes. That alone accounted for ~80% of my daily spend.
When I realized, I assumed this was normal (and even good): more bots meant better indexing, right? Turns out thatโs not totally true.
Bot management
Not all bots are good. Search engine crawlers need access to your site to index public pages and usually behave well. The problem starts when bots start aggressively hitting expensive uncached routes, or crawling the same pages endlessly. Those bots add no value, they just burn compute, cache writes and Edge requests.
There are several ways you can protect yourself from malicious bots. The easiest is using Vercel Firewall. Under Firewall โ Rules, Vercel provides some key protections.
Bot protection configuration in Vercel Firewall
| AI Bots | Bot Protection |
|---|---|
| Blocks known AI scrapers used to collect training data for LLMs. If youโre unsure whatโs hitting your site, start by enabling Logs only. Youโll get visibility without blocking anything. Once confirmed, you can safely enable blocking. | Vercel maintains an allowlist of legitimate crawlers, so Googlebot, Bing, etc. are not affected. โ ๏ธ this also blocks tools like curl, since theyโre not browser-like clients. If you rely on them, combine bot protection with more targeted rules or rate limits. |
[!note]
Blocking AI bots does not affect your presence in search engines or LLM recommendations.
Add a "human-safe" rate limit
Even with bot protection enabled, adding a soft rate limit is not a bad idea, especially for expensive or high-traffic paths. If you're unsure about how to configure it, here's a simple starting point:
Request Path - matches - /your-expensive-route/*
100 requests / minute / IP
Real users will (almost) never hit this, but this gives you an alternative or an extra protection against malitious bots and other attackers.
What about robots.txt?
A strict robots.txt helps, but by itself itโs not enough, since many abusive bots ignore it completely. It adds up, but probably not the solution if you're receiving a spike in traffict caused by bots.
Image Optimizations
Vercel strongly encourages using their Image component, and for good reasons: it improves performance, applies modern formats, and handles responsive images automatically. The catch is that it is a bit tricky to understand when transformations are beneficial vs when not, specially because every image transformation costs money.
The first time a specific image variant is requested (for a given size, format and quality), Vercel processes that image through an special optimization pipeline. That process generates a "transformation", which is then cached and reused for subsequent requests of that exact same variant. If the same image is later requested at a different size, format, or quality, Vercel has to generate (and charge for) a new variant.
Image optimization is most useful when:
- the same images are requested many times
- at the same sizes
- by real users
It becomes expensive when configuration restrictions are vague (many sizes or formats allowed) or when images are rarely reused (low traffic or they appear in few unimportant places in your site).
In my case, I was using images from TMDB, which already serves images at predefined sizes (w185, w500, w780, โฆ). By letting Next.js re-optimize them again, I was paying twice for work that was already done.
First rule: donโt optimize whatโs already optimized
If your image provider already gives you the exact size you need, disabling optimization is often the cheapest option. Youโll still pay for Fast Data Transfer, but you avoid paying for unnecessary transformations and cache writes.
This is not always possible (and not always obvious) so hereโs how to reduce image costs when you do rely on Vercelโs optimizer.
Reduce the number of variants
The biggest cost driver is the number of variants Vercel has to generate. By default, Next.js allows many sizes, formats, and qualities, which multiplies quickly (and most of the times is unnecessary).
Reducing your image configuration to a few sensible options can dramatically cut the number of transformations:
```ts title="next.config.ts"
module.exports = {
images: {
deviceSizes: [640, 1080, 2048],
imageSizes: [64, 384, 1024],
formats: ['image/webp'],
},
}
Fewer sizes means fewer variants. Fewer variants means fewer transformations.
If your images donโt change often, increasing `minimumCacheTTL` also helps by keeping optimized images cached for longer:
```ts title="next.config.ts"
module.exports = {
images: {
minimumCacheTTL: 12_592_000, // 30 days
},
}
Static images are basically free
Local images imported statically at build time are hashed and cached as immutable. Once built, they donโt trigger runtime transformations at all.
If an image is part of your UI or content and doesnโt change, prefer static imports whenever possible:
import img from './images/my-image.jpg'
return (
<Image src={img} /> // no need to specify 'sizes' for local assets
)
Donโt optimize images no one sees
Images outside the viewport are often requested and optimized even if users never scroll far enough to see them. Adding loading="lazy" prevents a lot of unnecessary work (especially from bots).
This wonโt stop all automated traffic, but it significantly reduces wasted transformations:
<Image ... loading="lazy" />
If none of the above works, turning optimization off is the right call
If image costs are still too high, donโt be afraid to selectively disable optimization using unoptimized.
This is especially useful for large lists or galleries where most images are rarely seen:
<>
{images.map(({ imageId, imageSrc, imageAlt }, index) => (
<Image key={imageId} src={imageSrc} alt={imageAlt} unoptimized={index < 20} />
))}
</>
Sometimes, serving images โas-isโ via the CDN can be cheaper than continuously transforming them.
Server-side costs (requests, compute and caching)
One big spend hole on Vercel usually comes from server-side work: requests that trigger code execution, database queries, or cache reads/writes. These usually fall into three categories:
- Fast Data Transfer: raw data sent from the CDN to users (assets, images, HTML,...)
- Fast Origin Transfer: server-side execution (Functions, Middleware, ISR)
- Edge Requests: how often a request reaches the Edge or origin, cached or not
If youโre investigating a cost spike, always start with your top paths. In most cases, the homepage is the main culprit.
The key thing to understand is that server-side costs scale with requests, not users.
One user loading a page once is cheap. Thousands of bots hitting the same page can become extremely expensive if each request triggers server work.
Journey of an Edge Request
Fast Data Transfer
Fast Data Transfer is mostly about how much data you send to users: JS bundles, images, fonts and static assets.
In practice, the biggest wins come from:
- not shipping assets that arenโt immediately needed
- lazy-loading anything outside the viewport
Fast Data Transfer is relatively cheap and often improves automatically as a side effect of other optimizations (image optimization, caching, etc.).
[!INFO]
Counter-intuitively, if image optimizations go down and Fast Data Transfer goes up, that can be a good sign; it usually means fewer transformations and more direct asset delivery. The key is finding the right balance to avoid FDT from exceeding the cost of transformations.[!TIP]
If Fast Data Transfer becomes your main cost driver, Vercel may simply not be the best platform for your use case. Consider migrating to Cloudflare or a custom VPS that fits your needs better.
Fast Origin Transfer
What makes this tricky is that most of this work is invisible. Nothing seems broken, everything goes fast, but every request is doing real work on the server. Every time a request:
- runs a Server Component
- hits a Route Handler
- executes Middleware
- regenerates an ISR page
youโre paying for compute. The most straightforward optimization we can do here relies on the middleware.
Middleware
Middlewares are functions that run automatically for every incoming request, allowing you to inspect or modify the request data before it reaches your application's routing system. In Next.js, you're charged for every invocation of the middleware, sometimes even twice (preflight + request), so reducing the number of times it runs is one of best way of reducing extra costs.
If your middleware only protects authenticated routes, donโt let it run on public pages. Use a matcher (Next.js comes with that feature built-in - Next.js Matcher).
Less middleware execution = fewer compute costs.
Edge Requests
This was one of the biggest issues in my case. The problem with this is that many of these issues are invisible in development: low traffic hides inefficiencies that only become expensive once real or automated traffic hits production.
My homepage was receiving most of the traffic and was doing 20+ server-side fetches, none of them cached. Every single visit triggered all of them again.That works fine at low traffic, but it scales pretty bad. This is especially easy to miss when using Server Components. Fetching data in the server feels "free" during development, but in production, every fetch runs on every request unless itโs explicitly cached.
Rule of thumb: if data is the same for most users and doesnโt change every minute, cache it.
You can cache at the page level:
export const revalidate = 3600 // 1 hour
Or at the fetch level
await fetch(url, { next: { revalidate: 3600 } })
Make sure โstaticโ pages are actually static
I explicitly marked some pages as static (ISR), but they were still rendered dynamically.
Why? Because calling Prisma directly inside a Server Component forces the page to be dynamic, and Next.js wonโt warn you.
Next.js can generate pages as dynamic even when you explicitly tagged them as static. To check it out, review the build output and ensure your routes are generated as "ISR" or Static if that's what you want.
Next.js tells you which routes are dynamic vs static after running npm/pnp/yarn run build
Reduce database calls
Every database query counts as work. Multiple queries per request add up very quickly.
A common pattern I saw many times looks like this (this is a simplified example, but it illustrates a very common pattern in server-rendered pages):
const movie = await prisma.movie.findUnique({ where: { id } })
const director = await prisma.person.findUnique({
where: { id: movie.directorId },
})
const relatedMovies = await prisma.movie.findMany({
where: { genre: movie.genre },
})
const watchEvents = await prisma.watchEvent.findMany({
where: { userId, movieId: id },
})
Thatโs four separate database calls for a single page view.
In many cases, this can be reduced to one or two queries using include or batching:
// Using Prisma's ORM built-in `include` feature
const movie = await prisma.movie.findUnique({
where: { id },
include: {
director: true,
relatedMovies: true,
watchEvents: {
where: { userId },
},
},
})
// OR you can parallelize non-dependent requests
const movie = await prisma.movie.findUnique({ where: { id } })
const directorPromise = prisma.person.findUnique({
where: { id: movie.directorId },
})
const relatedMoviesPromise = prisma.movie.findMany({
where: { genre: movie.genre },
})
const watchEventsPromise = prisma.watchEvent.findMany({
where: { userId, movieId: id },
})
// This still results in multiple database queries,
// but they run in parallel instead of sequentially,
// reducing total request time and compute usage.
const [director, relatedMovies, watchEvents] = await Promise.all([
directorPromise,
relatedMoviesPromise,
watchEventsPromise,
])
Reducing the number of queries is ideal, but when thatโs not possible, running them in parallel is still a meaningful improvement.
Avoid duplicate server calls (Metadata is the common trap)
I found this pattern to be extremely common:
- fetch data in
generateMetadata(for SEO) - fetch the same data again in the page component
Thatโs two server calls per request.
You can fix this with request deduplication using Reactโs cache:
import { cache } from 'react'
export const getMovieCached = cache(async (id: number) => {
return getAPIMovieDetails(id)
})
Now metadata and rendering share the same request.
To close this section, remember that most Vercel bills donโt rocketship because of one big mistake, but because of many small server-side decisions that little by little scale with traffic.
Extra: Add a Spend Management Limit
It seems obvious, but I already saw quite a few people in Twitter complaining after receiving a Vercel Bill of thousands of dollars in just one weekend.
If you're planning to publish your website to the world, configure a limit with which you're comfortable even when your traffic is very low (even if it is only you using your site).
You can also configure several notification thresholds and decide whether to suspend your site when the limit is reached or just get notified.
Cost Optimization Checklist (wrap-up)
If you're under one of this situations, you can expect a surprise in your bill almost for sure:
| Cause | Solution |
|---|---|
| You published a large sitemap | Enable Bot Protection / Rate Limiting |
| Your most visited pages do a lot of server-side fetching | Cache aggressively (ISR / revalidation) or move logic behind auth |
| You use Middleware globally | Remove runs from unnecessary pages using a matcher
|
You rely on <Image> with many sizes |
Reduce sizes + Increase caching time + Add unoptimized to some images |
Final Thoughts
We all have heard the statement: "Vercel is so expensive", and yes, it is, but most of the times the extra costs come for bad configuration, either coming from unexpected usage of your site or simply because traffic increase was not planned and you never faced this situation before, like it was in my case.
The good part is that I was able to reduce my bill by 92% in just a few days, learned a lot along the way and now my website is more robust and scalable, and it turns out is faster than ever after applying some of the improvements mentioned in this post ๐คฏ. One of the biggest takes from this experience: Optimizing for performance and optimizing for cost are related, but not the same thing.
Lesson learned, Vercel โ๏ธ๐ค
Useful resources
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.


