billing hacks
Understand Your BillCost Forensics

Cost Forensics

Most bills have one or two culprits. Here's how to find them in 30 minutes.

THE 80/20 RULE OF VERCEL BILLS20%routesfine80% of your costsA handful of routes drive most of your bill.Find them. Fix them. Done.

The Pattern

80% of costs come from 20% of routes. Usually the same five culprits: accidental SSR pages, chatty polling clients, N+1 queries, bloated API payloads, and arbitrary image sizes. This audit takes 30 minutes and tells you exactly where to swing the axe.

You don't need to optimize everything. You need to find the two or three routes that are bleeding money—and fix those.

This isn't dashboard browsing. It's forensic investigation.

The 30-Minute Audit

Set a timer. This process is designed to be fast and decisive.

30-MINUTE COST AUDIT1. Pull UsageCPU, Bandwidth,Functions, Images2. Find DominantWhich categoryis biggest?3. Drill Into DetailsSort by Duration,Invocations, GB-Hours4. Hit ListTop 3-5routes to fixFunctions/CPUBandwidthImages→ Rendering docs→ Bandwidth docs→ Image docs5 min2 min15 min8 min

Pull your usage summary (5 minutes)

Go to Settings → Usage in your Vercel dashboard. Set the date range to your last full billing cycle.

Write down four numbers:

  • Active CPU hours used
  • Bandwidth consumed (GB)
  • Function invocations (millions)
  • Image transformations

These are your baseline. Everything else is about finding where each one is coming from.

Find your dominant cost category (2 minutes)

Which line item is eating the most money? This determines where you start.

If The Biggest Number Is...Go Here First
Active CPU / FunctionsRendering → Functions
BandwidthBandwidth
Image TransformationsImage Optimization
Build MinutesPreview deployment settings

Don't spread your attention thin. Pick the biggest category and go deep.

Drill into the details (15 minutes)

Click into Observability for your production deployment. Create three lists:

Sort by Duration → Top 5 slowest functions Sort by Invocations → Top 5 most-called endpoints Sort by GB-Hours → Top 5 resource hogs

Write them down. These 15 routes are your investigation targets.

The Five Usual Suspects

In our audits, the same five patterns appear over and over. Your problem routes almost certainly match one of these profiles.

Suspect 1: The Accidental SSR Page

The crime: A page that should be static is secretly running server-side on every single request.

Evidence:

  • High invocation count for what looks like a marketing page
  • Route shows ƒ (dynamic) in build output
  • Content doesn't actually change between requests

How to catch it:

npm run build
# Look for routes marked ƒ that seem like they should be static

Then grep for these dynamic triggers:

  • cookies() or headers() calls (even buried deep in imports)
  • searchParams without generateStaticParams
  • export const dynamic = 'force-dynamic'

Sentence: 90-99% cost reduction when you fix it

Suspect 2: The Chatty Client

The crime: An API route getting hammered by aggressive client-side polling. Death by a thousand cuts.

Evidence:

  • API route with astronomical invocation count
  • Duration per request is tiny (5-20ms)
  • Volume is the killer, not complexity

How to catch it: Search your client code for these patterns:

// Polling every 5 seconds - 720 requests/hour/user
useEffect(() => {
  const interval = setInterval(fetchData, 5000);
  return () => clearInterval(interval);
}, []);

// Or SWR/React Query defaults that refetch on every focus, mount, reconnect...
const { data } = useSWR('/api/status');

The fix:

// 12x fewer requests
const interval = setInterval(fetchData, 60000);

// Or configure SWR to chill out
const { data } = useSWR('/api/status', fetcher, {
  refreshInterval: 60000,
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
});

Sentence: 50-90% cost reduction

Suspect 3: The N+1 Query Monster

The crime: An API route that fires 50+ database queries when it should fire 2-3. Every user fetches a list, and for each item in that list, you fire another query. Hence N+1.

Evidence:

  • Moderate invocation count but very high duration
  • Database time dominates the trace
  • Usually on list pages, dashboards, or detail views with related data

How to catch it: Temporarily instrument your database calls:

let queryCount = 0;
const originalQuery = db.query;
db.query = (...args) => {
  queryCount++;
  return originalQuery(...args);
};

// ... your handler logic ...

console.log(`Queries executed: ${queryCount}`);

If a single request fires 20+ queries, you've got an N+1. If it's 50+, you've got a serious problem.

Sentence: 60-80% duration reduction (which means CPU cost reduction)

Suspect 4: The Bloated Response

The crime: Returning entire database records when clients only need three fields. Bandwidth eaten by invisible JSON.

Evidence:

  • High bandwidth consumption relative to traffic
  • API responses are surprisingly large
  • You're returning SELECT * or full Prisma records

How to catch it:

curl -s https://your-app.vercel.app/api/endpoint | wc -c
# See the size in bytes. Surprise yourself.

Or in DevTools: Network tab → find the request → check "Size" column. The "transferred" vs "actual" size tells you if compression is working.

If an endpoint returns 50KB when the UI only displays 5 fields, you're sending 45KB of wasted bytes on every request.

Sentence: 40-70% bandwidth reduction for that endpoint

Suspect 5: The Arbitrary Image

The crime: Using random image dimensions like width={347} instead of standardized breakpoints. Each unique size = a new transformation = more money.

Evidence:

  • High image transformation count
  • Many unique size variants in your logs
  • Developers eyeballing widths in code

How to catch it:

grep -r "width=" --include="*.tsx" --include="*.jsx" | grep -v node_modules

Count unique widths. If you have more than 5-6 standard sizes, you're creating unnecessary transformations. width={347}, width={348}, and width={350} should all be width={360} or whatever your design system dictates.

Sentence: 20-50% reduction in image transformation costs

Building Your Hit List

Now turn your investigation into a prioritized action plan. Format it like this:

PrioritySuspectRouteEst. SavingsEffortFix
1Accidental SSR/products/[id]~95%MediumRemove cookies() call
2Chatty client/api/status~80%LowIncrease polling to 60s
3N+1 queries/api/feed~70%MediumAdd include to Prisma
4Bloated response/api/search~50%LowSelect only needed fields
5Arbitrary imagesProductCard~30%LowStandardize to 5 widths

The rule: Start with high-impact, low-effort. Knock out a few quick wins first—momentum matters.

Don't Wait for the Bill

Set up alerts so you catch regressions before they hit your invoice.

Built-in Alerts (2 minutes)

  1. Go to Settings → Notifications
  2. Enable usage alerts
  3. Set thresholds at 50%, 75%, 90% of your included amounts

When you hit 75%, you've got time to investigate. When you hit 90%, you're in the danger zone.

Custom Instrumentation (Optional)

For teams that want detailed per-route tracking:

lib/metrics.ts
export async function trackExecution(
  route: string,
  durationMs: number,
  metadata?: Record<string, string>
) {
  // Send to your observability platform (Datadog, Axiom, etc.)
  await fetch('https://your-metrics-service.com/ingest', {
    method: 'POST',
    body: JSON.stringify({
      metric: 'function_duration',
      route,
      duration: durationMs,
      ...metadata,
      timestamp: Date.now(),
    }),
  });
}

// Usage
export async function GET(request: Request) {
  const start = performance.now();
  // ... your logic ...
  await trackExecution('/api/products', performance.now() - start);
  return Response.json(data);
}

This gives you route-level visibility that Vercel's dashboard doesn't provide out of the box.

Monthly Health Check

Put a 15-minute calendar reminder for the first of every month. Check for these red flags:

Red FlagWhat It MeansAction
Single route > 30% of CPUOne page is dominating costsInvestigate that route
Bandwidth growing faster than trafficResponses are getting bloatedAudit payload sizes
Preview costs > 10% of totalToo many preview deploymentsTighten branch filtering
Image transformations risingArbitrary sizing creeping backAudit width values
New ƒ routes in build outputSomeone added dynamic pagesCheck if they should be static

These are early warning signs. Catch them before they become expensive.

Verifying Your Fixes

After implementing optimizations, don't just hope they worked. Verify.

Wait one full billing cycle

Partial data misleads. Wait for a complete month before drawing conclusions.

Compare apples to apples

Same metrics, same time range. "Function duration dropped 40% month-over-month" is a meaningful comparison.

Confirm deployment

Sounds obvious, but double-check: did your fix actually reach production? Check the deployment timestamp.

Document what worked

Write it down. "Removed cookies() from /products/[id], reduced invocations 95%." Future you will thank present you.

The Practice

Cost optimization isn't a project. It's a practice. Monthly audits take 15 minutes and catch drift before it becomes expensive. Put it on your calendar.


Ready to fix what you found? Start with Rendering Strategy for the biggest wins, then hit Functions for code-level optimization.

On this page