<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ricardo Martin</title>
    <description>The latest articles on DEV Community by Ricardo Martin (@ricardo_martin_0abeba3f35).</description>
    <link>https://dev.to/ricardo_martin_0abeba3f35</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3735993%2F04ed4ba6-9345-4418-996e-2fa6270d8e98.jpg</url>
      <title>DEV Community: Ricardo Martin</title>
      <link>https://dev.to/ricardo_martin_0abeba3f35</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricardo_martin_0abeba3f35"/>
    <language>en</language>
    <item>
      <title>How to reduce your Vercel bill</title>
      <dc:creator>Ricardo Martin</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:29:00 +0000</pubDate>
      <link>https://dev.to/ricardo_martin_0abeba3f35/how-to-reduce-your-vercel-bill-48e6</link>
      <guid>https://dev.to/ricardo_martin_0abeba3f35/how-to-reduce-your-vercel-bill-48e6</guid>
      <description>&lt;p&gt;A couple of weeks ago, I got an email from Vercel: 50% of the credits included in my Pro plan were already gone. &lt;strong&gt;Only 2 days into my new billing cycle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For context, I’m working on a pet-project: &lt;a href="https://framecount.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;Framecount&lt;/strong&gt;&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;92,37%&lt;/strong&gt; almost instantly.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkyl2tz9ju2d4zvq5qqk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkyl2tz9ju2d4zvq5qqk.webp" alt=" " width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  My journey to reduce Vercel costs
&lt;/p&gt;

&lt;h3&gt;
  
  
  How Vercel bills you (a mental model)
&lt;/h3&gt;

&lt;p&gt;Most Vercel bills come from 4 buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Typical causes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image Transformations&lt;/td&gt;
&lt;td&gt;On-demand image resizing &amp;amp; encoding&lt;/td&gt;
&lt;td&gt;Abusing &lt;code&gt;next/image&lt;/code&gt; + many sizes in config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast Data Transfer&lt;/td&gt;
&lt;td&gt;Static assets served from CDN → user traffic&lt;/td&gt;
&lt;td&gt;Large pages + a lot of static assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast Origin Transfer&lt;/td&gt;
&lt;td&gt;Functions, ISR, Middleware&lt;/td&gt;
&lt;td&gt;Server fetches and unwanted side-effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge Requests&lt;/td&gt;
&lt;td&gt;Cached &amp;amp; uncached requests&lt;/td&gt;
&lt;td&gt;Big amount or uncached requests (Bots, crawlers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Vercel Firewall: The Biggest Win
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;When I realized, I assumed this was normal (and even good): more bots meant better indexing, right? Turns out that’s not totally true.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bot management
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;There are several ways you can protect yourself from malicious bots. The easiest is using Vercel Firewall. Under &lt;strong&gt;Firewall → Rules&lt;/strong&gt;, Vercel provides some key protections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nws3skfga4tqdud69j1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nws3skfga4tqdud69j1.webp" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  Bot protection configuration in Vercel Firewall
&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AI Bots&lt;/th&gt;
&lt;th&gt;Bot Protection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;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.&lt;/td&gt;
&lt;td&gt;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.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;[!note]&lt;br&gt;
Blocking AI bots does not affect your presence in search engines or LLM recommendations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Add a "human-safe" rate limit
&lt;/h4&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request Path - matches - /your-expensive-route/*
100 requests / minute / IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
Real users will (almost) never hit this, but this gives you an alternative or an extra protection against malitious bots and other attackers.
&lt;/p&gt;

&lt;h4&gt;
  
  
  What about &lt;code&gt;robots.txt&lt;/code&gt;?
&lt;/h4&gt;

&lt;p&gt;A strict &lt;code&gt;robots.txt&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image Optimizations
&lt;/h3&gt;

&lt;p&gt;Vercel strongly encourages using their &lt;code&gt;Image&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The first time a specific image variant is requested (for a given &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;quality&lt;/code&gt;), Vercel processes that image through an special optimization pipeline. That process generates a "transformation", which is then cached and reused for subsequent requests &lt;strong&gt;of that exact same variant&lt;/strong&gt;. If the same image is later requested at a different &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, or &lt;code&gt;quality&lt;/code&gt;, Vercel has to generate (and charge for) a new variant.&lt;/p&gt;

&lt;p&gt;Image optimization is most useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same images are requested many times&lt;/li&gt;
&lt;li&gt;at the same sizes&lt;/li&gt;
&lt;li&gt;by real users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;In my case, I was using images from &lt;em&gt;TMDB&lt;/em&gt;, which already serves images at predefined sizes (&lt;code&gt;w185&lt;/code&gt;, &lt;code&gt;w500&lt;/code&gt;, &lt;code&gt;w780&lt;/code&gt;, …). By letting Next.js re-optimize them again, I was paying twice for work that was already done.&lt;/p&gt;

&lt;h4&gt;
  
  
  First rule: don’t optimize what’s already optimized
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  Reduce the number of variants
&lt;/h4&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;Reducing your image configuration to a few sensible options can dramatically cut the number of transformations:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts title="next.config.ts"&lt;br&gt;
module.exports = {&lt;br&gt;
  images: {&lt;br&gt;
    deviceSizes: [640, 1080, 2048],&lt;br&gt;
    imageSizes: [64, 384, 1024],&lt;br&gt;
    formats: ['image/webp'],&lt;br&gt;
  },&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


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
  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Static images are basically free
&lt;/h4&gt;

&lt;p&gt;Local images imported statically at build time are hashed and cached as immutable. Once built, they don’t trigger runtime transformations at all.&lt;/p&gt;

&lt;p&gt;If an image is part of your UI or content and doesn’t change, prefer static imports whenever possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./images/my-image.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// no need to specify 'sizes' for local assets&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Don’t optimize images no one sees
&lt;/h4&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;This won’t stop all automated traffic, but it significantly reduces wasted transformations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  If none of the above works, turning optimization off is the right call
&lt;/h4&gt;

&lt;p&gt;If image costs are still too high, don’t be afraid to selectively disable optimization using unoptimized.&lt;/p&gt;

&lt;p&gt;This is especially useful for large lists or galleries where most images are rarely seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageSrc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageAlt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageSrc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageAlt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;unoptimized&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes, serving images “as-is” via the CDN can be cheaper than continuously transforming them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side costs (requests, compute and caching)
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast Data Transfer&lt;/strong&gt;: raw data sent from the CDN to users (assets, images, HTML,...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast Origin Transfer:&lt;/strong&gt; server-side execution (Functions, Middleware, ISR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Requests:&lt;/strong&gt; how often a request reaches the Edge or origin, cached or not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re investigating a cost spike, always start with your top paths. In most cases, the homepage is the main culprit.&lt;/p&gt;

&lt;p&gt;The key thing to understand is that server-side costs scale with requests, not users.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfgs8a7n1geap1rus116.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfgs8a7n1geap1rus116.webp" alt=" " width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  Journey of an Edge Request
&lt;/p&gt;

&lt;h4&gt;
  
  
  Fast Data Transfer
&lt;/h4&gt;

&lt;p&gt;Fast Data Transfer is mostly about how much data you send to users: JS bundles, images, fonts and static assets.&lt;/p&gt;

&lt;p&gt;In practice, the biggest wins come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not shipping assets that aren’t immediately needed&lt;/li&gt;
&lt;li&gt;lazy-loading anything outside the viewport&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast Data Transfer is relatively cheap and often improves automatically as a side effect of other optimizations (image optimization, caching, etc.).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!INFO]&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;[!TIP]&lt;br&gt;
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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Fast Origin Transfer
&lt;/h4&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs a Server Component&lt;/li&gt;
&lt;li&gt;hits a Route Handler&lt;/li&gt;
&lt;li&gt;executes Middleware&lt;/li&gt;
&lt;li&gt;regenerates an ISR page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you’re paying for compute. The most straightforward optimization we can do here relies on the middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 - &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/proxy#matcher" rel="noopener noreferrer"&gt;Next.js Matcher&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Less middleware execution = fewer compute costs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Edge Requests
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;My homepage was receiving most of the traffic and was doing 20+ server-side fetches, &lt;em&gt;none of them cached&lt;/em&gt;. 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rule of thumb: if data is the same for most users and doesn’t change every minute, cache it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can cache at the page level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or at the fetch level&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make sure “static” pages are actually static&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I explicitly marked some pages as static (ISR), but they were still rendered dynamically.&lt;/p&gt;

&lt;p&gt;Why? Because calling Prisma directly inside a Server Component forces the page to be dynamic, and Next.js won’t warn you.&lt;/p&gt;

&lt;p&gt;Next.js can generate pages as &lt;code&gt;dynamic&lt;/code&gt; even when you explicitly tagged them as &lt;code&gt;static&lt;/code&gt;. To check it out, review the &lt;code&gt;build&lt;/code&gt; output and ensure your routes are generated as "ISR" or Static if that's what you want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyigq8a5qp9l13defb5j.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyigq8a5qp9l13defb5j.webp" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  Next.js tells you which routes are dynamic vs static after running &lt;code&gt;npm/pnp/yarn run build&lt;/code&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduce database calls&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every database query counts as work. Multiple queries per request add up very quickly.&lt;/p&gt;

&lt;p&gt;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):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;director&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;directorId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relatedMovies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watchEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;movieId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s &lt;strong&gt;four separate database calls&lt;/strong&gt; for a single page view.&lt;/p&gt;

&lt;p&gt;In many cases, this can be reduced to one or two queries using &lt;code&gt;include&lt;/code&gt; or batching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Using Prisma's ORM built-in `include` feature&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;director&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;relatedMovies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;watchEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// OR you can parallelize non-dependent requests&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directorPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;directorId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relatedMoviesPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchEventsPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watchEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;movieId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// This still results in multiple database queries,&lt;/span&gt;
&lt;span class="c1"&gt;// but they run in parallel instead of sequentially,&lt;/span&gt;
&lt;span class="c1"&gt;// reducing total request time and compute usage.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;director&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relatedMovies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchEvents&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;directorPromise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;relatedMoviesPromise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;watchEventsPromise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reducing the number of queries is ideal, but when that’s not possible, running them in parallel is still a meaningful improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid duplicate server calls (&lt;code&gt;Metadata&lt;/code&gt; is the common trap)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I found this pattern to be extremely common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch data in &lt;code&gt;generateMetadata&lt;/code&gt; (for SEO)&lt;/li&gt;
&lt;li&gt;fetch the same data again in the page component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s two server calls per request.&lt;/p&gt;

&lt;p&gt;You can fix this with request deduplication using React’s &lt;code&gt;cache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMovieCached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getAPIMovieDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now metadata and rendering share the same request.&lt;/p&gt;




&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extra: Add a Spend Management Limit
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;You can also configure several notification thresholds and decide whether to suspend your site when the limit is reached or just get notified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Optimization Checklist (wrap-up)
&lt;/h3&gt;

&lt;p&gt;If you're under one of this situations, you can expect a surprise in your bill almost for sure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You published a large sitemap&lt;/td&gt;
&lt;td&gt;Enable Bot Protection / Rate Limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your most visited pages do a lot of server-side fetching&lt;/td&gt;
&lt;td&gt;Cache aggressively (ISR / revalidation) or move logic behind auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You use Middleware globally&lt;/td&gt;
&lt;td&gt;Remove runs from unnecessary pages using a &lt;code&gt;matcher&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You rely on &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; with many sizes&lt;/td&gt;
&lt;td&gt;Reduce sizes + Increase caching time + Add &lt;code&gt;unoptimized&lt;/code&gt; to some images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Lesson learned, Vercel ☝️🤓&lt;/p&gt;

&lt;h4&gt;
  
  
  Useful resources
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/pricing/manage-and-optimize-usage" rel="noopener noreferrer"&gt;Vercel pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/life-of-a-vercel-request-what-happens-when-a-user-presses-enter" rel="noopener noreferrer"&gt;Vercel Blog: "Life of a Vercel request" article series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vercel</category>
      <category>webdev</category>
      <category>infrastructure</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Keep your Claude Code cost, usage, and context info always visible</title>
      <dc:creator>Ricardo Martin</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:20:45 +0000</pubDate>
      <link>https://dev.to/ricardo_martin_0abeba3f35/keep-your-claude-code-cost-usage-and-context-info-always-visible-47pp</link>
      <guid>https://dev.to/ricardo_martin_0abeba3f35/keep-your-claude-code-cost-usage-and-context-info-always-visible-47pp</guid>
      <description>&lt;p&gt;As LLMs become better and our AI usage grows, managing token usage as well as model's context window has become increasingly important.&lt;/p&gt;

&lt;p&gt;Three months ago, I switched to Claude Code from Cursor, and I quickly started to miss having that information always visible. A few weeks in, I found myself constantly hitting the &lt;code&gt;/usage&lt;/code&gt; command to check how the session was performing in terms of cost, token input/output and overall context window status.&lt;/p&gt;

&lt;p&gt;I was wondering: If all that data already exists, I should be able to bring it wherever I want and see it change in real time. And it turns out it is not only possible but also quite easy to do.&lt;/p&gt;

&lt;p&gt;The key component we're looking for is the &lt;strong&gt;status line&lt;/strong&gt;. That thin row under your prompt can become a small dashboard, tailored to exactly the numbers you care about (and it is highly customizable!). Here is how to set it up, and a few things the docs do not tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the status line actually is
&lt;/h2&gt;

&lt;p&gt;The status line is not a fixed set of toggles. It is &lt;strong&gt;a command you define&lt;/strong&gt;. Claude Code runs it, pipes a JSON blob describing the current session into it via &lt;code&gt;stdin&lt;/code&gt;, and prints whatever your command writes to &lt;code&gt;stdout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the mental model, and this is the important part: if you can compute it in a script, you can show it. Cost, model, branch, context percentage, failing tests, the ticket you are on, etc.&lt;/p&gt;

&lt;p&gt;The JSON output looks roughly like this:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"display_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Opus"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workspace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"current_dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/you/projects/app"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4213&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_lines_added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;156&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_lines_removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"used_percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1822&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  The fast way: let Claude write it
&lt;/h2&gt;

&lt;p&gt;Claude Code ships a command that configures a status line &lt;em&gt;for&lt;/em&gt; you: &lt;code&gt;/statusline&lt;/code&gt;. It takes plain-English instructions, so you can be specific — for example, &lt;code&gt;/statusline show the model, git branch, context percentage and session cost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude writes the script, drops it in &lt;code&gt;~/.claude/&lt;/code&gt;, and wires it into your settings. If you just want to get started, you can stop reading here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advanced way: creating and configuring your own script
&lt;/h2&gt;

&lt;p&gt;If you're nitpicky like me, the alternative is to write the script yourself, so it shows exactly what you want and nothing else. I run Claude Code inside &lt;a href="https://www.warp.dev/" rel="noopener noreferrer"&gt;Warp&lt;/a&gt;, which already surfaces things like the working directory and git branch in its own UI, so I deliberately left those &lt;em&gt;out&lt;/em&gt; of the status line and kept only the session numbers I can't get anywhere else: the model, cost, context usage, token input/output, and lines changed by the agent in the current session.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.claude/statusline.sh&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.model.display_name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.cost.total_cost_usd // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;     | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.context_window.used_percentage // 0'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;in_tok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.context_window.total_input_tokens // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;out_tok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.context_window.total_output_tokens // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;added&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.cost.total_lines_added // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;removed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.cost.total_lines_removed // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"[%s] &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;%.4f | ctx %s%% | in %s | out %s | +%s/-%s"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$in_tok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out_tok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$added&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$removed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Which renders as a single row:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Opus 4.8 (1M context)] $6.8190 | ctx 10% | in 99201 | out 1822 | +188/-6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;A couple of details worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;// 0&lt;/code&gt; in each &lt;code&gt;jq&lt;/code&gt; filter acts as a fallback. Some fields are &lt;code&gt;null&lt;/code&gt; before the first API response of a session (and after a &lt;code&gt;/compact&lt;/code&gt;), so without it you would get an empty value printed in those cases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context_window.total_input_tokens&lt;/code&gt; and &lt;code&gt;total_output_tokens&lt;/code&gt; are the tokens currently &lt;em&gt;in the window&lt;/em&gt; (input already includes cache reads and writes), not cumulative session totals — so they track what is actually loaded right now, next to the &lt;code&gt;ctx&lt;/code&gt; percentage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make it executable:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.claude/statusline.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;And point your settings at it in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/statusline.sh"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Settings reload on their own, but the line only redraws on your next interaction with Claude Code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6vnpph78hykp3df0hg6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6vnpph78hykp3df0hg6.png" alt="My Claude Code status line showing model, cost, context usage, token input/output, and lines changed" width="798" height="158"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;This is what mine looks like in a real session
  &lt;p&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Test the script before wiring it up, so you don't end up debugging blind inside Claude Code. Pipe it some mock JSON and check the output:&lt;/p&gt;
&lt;/blockquote&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"model":{"display_name":"Opus"},"cost":{"total_cost_usd":0.42,"total_lines_added":10,"total_lines_removed":2},"context_window":{"used_percentage":42,"total_input_tokens":99201,"total_output_tokens":1822}}'&lt;/span&gt; | ~/.claude/statusline.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Tips the docs don't explain
&lt;/h2&gt;

&lt;p&gt;A few things I found out after using it for a while:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch context, not just cost.&lt;/strong&gt; Cost is the number we naturally reach for, but &lt;code&gt;context_window.used_percentage&lt;/code&gt; is the one that actually changes how you work. Seeing the window fill up tells you &lt;em&gt;when&lt;/em&gt; to &lt;code&gt;/compact&lt;/code&gt; or start fresh &lt;strong&gt;before&lt;/strong&gt; quality degrades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh on a timer for anything time-based.&lt;/strong&gt; Updates are event-driven (after each message, after &lt;code&gt;/compact&lt;/code&gt;, and so on), which means they go quiet when the session is idle (e.g. while background subagents churn). Add &lt;code&gt;"refreshInterval": 5&lt;/code&gt; next to your &lt;code&gt;command&lt;/code&gt; to also re-run every N seconds and keep a clock or live git state fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Pro/Max, you can surface rate limits.&lt;/strong&gt; The JSON carries &lt;code&gt;rate_limits.five_hour&lt;/code&gt; and &lt;code&gt;rate_limits.seven_day&lt;/code&gt; with a &lt;code&gt;used_percentage&lt;/code&gt; each. This is handy if you're using the latest models in &lt;code&gt;xhigh&lt;/code&gt; or higher thinking mode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
If you prefer not having to maintain the script yourself, there are some community projects out there like &lt;a href="https://github.com/sirmalloc/ccstatusline" rel="noopener noreferrer"&gt;ccstatusline&lt;/a&gt; that give you a themeable status line with a configuration UI, no bash required. This will get you much fancier results in no time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bonus: an overview of your subagents, too
&lt;/h2&gt;

&lt;p&gt;There's a sibling setting for showing some of the information we have in our statusline for each subagent spawned within the same section too: the &lt;strong&gt;&lt;code&gt;subagentStatusLine&lt;/code&gt;&lt;/strong&gt;, which lets you reformat every row in the agent panel the way you reformat the main line:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subagentStatusLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/subagent-statusline.sh"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Before wiring it up, two things worth knowing so you don't end up debugging the wrong thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It styles the in-session agent panel, not the sessions manager.&lt;/strong&gt; These are two different screens. The full-screen dashboard you reach with &lt;code&gt;claude agents&lt;/code&gt; lists your independent &lt;em&gt;sessions&lt;/em&gt; across projects, and &lt;code&gt;subagentStatusLine&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; touch that one. What it customizes is the panel of &lt;em&gt;subagents&lt;/em&gt; that appears right below your prompt when you are inside a session and Claude spawns child agents (through its Task tool). So to see your script run: open a session, spawn some subagents, and watch the rows beneath the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is not one of the fields.&lt;/strong&gt; Unlike the main status line, the per-agent payload has no &lt;code&gt;cost&lt;/code&gt; object at the moment, so you cannot show a dollar figure next to each agent. What you &lt;em&gt;do&lt;/em&gt; get per task is &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;tokenCount&lt;/code&gt;, and &lt;code&gt;contextWindowSize&lt;/code&gt; — which is the barely minimum (and to me, more than enough) for a context percentage, arguably the more useful signal for an agent that is still running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command runs once per refresh and receives &lt;strong&gt;every visible agent at once&lt;/strong&gt;, as a &lt;code&gt;tasks&lt;/code&gt; array on &lt;code&gt;stdin&lt;/code&gt;. You answer with &lt;strong&gt;one JSON line per row&lt;/strong&gt; you want to override, in the shape &lt;code&gt;{"id": "…", "content": "…"}&lt;/code&gt;. Here is a starting point that shows each agent's name, model, context usage, and status — adapt it to taste:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.claude/subagent-statusline.sh&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Emit one {id, content} line per agent: name · model · context % · status&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
  .tasks[]
  | select(.contextWindowSize and .tokenCount)
  | { id,
      content: "\(.name)  ·  \(.model // "…")  ·  \(((.tokenCount * 100) / .contextWindowSize) | floor)% ctx  ·  \(.status)" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Make it executable (&lt;code&gt;chmod +x ~/.claude/subagent-statusline.sh&lt;/code&gt;) and it takes over each agent row.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
The per-agent &lt;code&gt;model&lt;/code&gt; and &lt;code&gt;contextWindowSize&lt;/code&gt; fields need Claude Code &lt;strong&gt;v2.1.205 or later&lt;/strong&gt;, and are omitted for an agent whose model has not resolved yet. The &lt;code&gt;select(...)&lt;/code&gt; above just leaves those rows with their default formatting until it does. Any row whose &lt;code&gt;id&lt;/code&gt; you don't return keeps the default &lt;code&gt;name · description · token count&lt;/code&gt;, and an empty &lt;code&gt;content&lt;/code&gt; hides a row entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4aubg95wms231kqv87d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4aubg95wms231kqv87d.png" alt="A custom subagent status line showing each agent's model and context usage in the in-session subagent panel" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;&lt;br&gt;
    Each agent's model and context usage at a glance in the subagent panel below the prompt&lt;br&gt;
  
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why having this information in real time helps
&lt;/h2&gt;

&lt;p&gt;Even though you have the Max plan, in my opinion it is a good idea to keep track of your context, cost and overall usage for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time cost awareness.&lt;/strong&gt; Watching spend accumulate nudges better decisions about when to open a fresh session or compact context, as well as understanding how you're being charged based on the model and thinking mode you're using. In my case, I was able to get the same results 95% of the times by reducing the cost ~40%, just by playing with the model and thinking mode. Raising awareness of real API cost usage is an increasingly important skill to develop, to be ready for when the party is over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowing when to reset the context.&lt;/strong&gt; Cost tells you what a session has spent; the context percentage tells you when it's about to get &lt;em&gt;worse&lt;/em&gt;. It has been demonstrated that model quality degrades as the window fills up, as agents tend to reduce thinking cycles if they know the remaining context is small, so watching &lt;code&gt;ctx&lt;/code&gt; climb is my clue to &lt;code&gt;/compact&lt;/code&gt; or start fresh &lt;strong&gt;before&lt;/strong&gt; the answers start slipping, instead of noticing after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catching a runaway session early.&lt;/strong&gt; When the token or cost counters climb faster than the requested task complexity suggests, that's usually a sign something is off: an agent stuck in a loop, re-reading huge files, or running a heavier thinking mode than it needs. Having the numbers in front of me means I catch it fast and I can step in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go deeper
&lt;/h2&gt;

&lt;p&gt;To deep dive on this feature; every JSON field, the &lt;code&gt;/statusline&lt;/code&gt; command, &lt;code&gt;padding&lt;/code&gt;, multi-line output, colors, and clickable links, visit the official docs: &lt;a href="https://code.claude.com/docs/en/statusline" rel="noopener noreferrer"&gt;code.claude.com/docs/en/statusline&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
