billing hacks

Writing Cheaper Functions

Code patterns that slash Active CPU time. Think of it like a taxi meter that only runs while you're moving.

THE CPU METER5ms15ms10msDB query (200ms) — FREEAPI call (180ms) — FREEBILLED: 30ms(only green blocks)TOTAL: 410ms(what users wait)

The Mental Model

Picture a taxi meter that only ticks while the wheels are turning. Stopped at a red light? Meter pauses. That's Active CPU billing—you only pay when JavaScript is actually executing. Waiting for Postgres? Free. Waiting for Stripe? Free. Only the green blocks cost money.

This flips everything you know about performance optimization.

The old game was making requests faster. The new game is making your JavaScript do less—and letting databases, APIs, and caches do the heavy lifting.

The Five-Move Playbook

There are exactly five ways to reduce Active CPU costs. Every optimization is one of these:

MoveWhat It MeansTypical Savings
1. Don't run at allCache the response100% (literally free)
2. Bail earlyExit before expensive work30-70%
3. Offload to DBLet SQL do the math40-60%
4. Shrink the workReturn less data20-40%
5. ParallelizeSame work, faster UX0% savings, 2-4x faster
The Five-Move Playbook: Cache → Bail Early → Offload to DB → Shrink Work → Parallelize

Notice that last one: parallelization doesn't save money—I/O wait is free either way. But your users wait half as long, and that's worth doing.

The Weird Implication

A function that runs 10ms of JS then waits 5 seconds for a slow API costs exactly the same as one that runs 10ms then waits 50ms. Duration doesn't matter. Only the green blocks on the meter.

Move 5 in Action: Parallelize Everything

A booking confirmation page needs four pieces of data: booking details, customer profile, venue info, payment status. Here's how most developers write it—and why it's costing them UX, not money.

// app/bookings/[id]/page.tsx
export default async function BookingPage({ params }) {
  // Each await blocks the next one
  const booking = await db.bookings.findUnique({
    where: { id: params.id }
  });

  const customer = await db.customers.findUnique({
    where: { id: booking.customerId }
  });

  const venue = await db.venues.findUnique({
    where: { id: booking.venueId }
  });

  const payment = await stripe.paymentIntents.retrieve(
    booking.paymentIntentId
  );

  return <BookingConfirmation {...{ booking, customer, venue, payment }} />;
}
MetricValue
Wall clock800ms (200ms × 4)
Active CPU~40ms
Cost$X
User experienceSluggish
// app/bookings/[id]/page.tsx
export default async function BookingPage({ params }) {
  // Get the booking first (need IDs for other queries)
  const booking = await db.bookings.findUnique({
    where: { id: params.id }
  });

  if (!booking) notFound();

  // Fire everything else at once
  const [customer, venue, payment] = await Promise.all([ 
    db.customers.findUnique({ where: { id: booking.customerId } }),
    db.venues.findUnique({ where: { id: booking.venueId } }),
    stripe.paymentIntents.retrieve(booking.paymentIntentId),
  ]);

  return <BookingConfirmation {...{ booking, customer, venue, payment }} />;
}
MetricValue
Wall clock400ms (200ms + 200ms)
Active CPU~40ms
Cost$X (same!)
User experienceSnappy

The punchline: parallelization is free UX improvement. Same cost, half the wait. There's no reason not to do this everywhere.

Move 3 in Action: Make the Database Do Math

Here's a dashboard API that fetches analytics across multiple time periods. One version is chatty. The other is smart.

export async function GET() {
  // Each query = JS overhead for parsing, sending, receiving
  const today = await db.events.count({
    where: { createdAt: { gte: startOfToday() } }
  });

  const thisWeek = await db.events.count({
    where: { createdAt: { gte: startOfWeek() } }
  });

  const thisMonth = await db.events.count({
    where: { createdAt: { gte: startOfMonth() } }
  });

  const byType = await db.events.groupBy({
    by: ['type'],
    _count: true,
  });

  const bySource = await db.events.groupBy({
    by: ['source'],
    _count: true,
  });

  return Response.json({ today, thisWeek, thisMonth, byType, bySource });
}

5 queries → 5 round trips → 5× the JavaScript execution overhead for parsing, serializing, and handling results.

export async function GET() {
  // One trip. Let Postgres do all the counting.
  const stats = await db.$queryRaw`
    SELECT
      COUNT(*) FILTER (WHERE created_at >= ${startOfToday()}) as today,
      COUNT(*) FILTER (WHERE created_at >= ${startOfWeek()}) as this_week,
      COUNT(*) FILTER (WHERE created_at >= ${startOfMonth()}) as this_month,
      jsonb_object_agg(type, type_count) as by_type,
      jsonb_object_agg(source, source_count) as by_source
    FROM events
    CROSS JOIN (
      SELECT type, COUNT(*) as type_count FROM events GROUP BY type
    ) t
    CROSS JOIN (
      SELECT source, COUNT(*) as source_count FROM events GROUP BY source
    ) s
  `;

  return Response.json(stats[0]);
}

1 query → 1 round trip → minimal JavaScript. The database does the aggregation—and that time is free.

The principle is brutal in its simplicity: SQL execution time costs nothing. JavaScript execution time costs money. Push every calculation you can into the database.

Move 2 in Action: Bail Before the Expensive Part

Every line of JavaScript that executes costs money. The trick is ordering your code so cheap checks happen first—and expensive work only runs when absolutely necessary.

Here's a Stripe webhook handler with layered bailouts:

app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  // LAYER 1: Header check (nanoseconds)
  const signature = request.headers.get('stripe-signature');
  if (!signature) {
    return Response.json({ error: 'Missing signature' }, { status: 400 });
  }

  // LAYER 2: Signature validation (microseconds)
  const rawBody = await request.text();
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      rawBody,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // LAYER 3: Event type filter (microseconds)
  const handledEvents = ['checkout.session.completed', 'invoice.paid'];
  if (!handledEvents.includes(event.type)) {
    // Acknowledge but don't process - Stripe sends dozens of event types
    return Response.json({ received: true });
  }

  // LAYER 4: The expensive work (milliseconds)
  // Only runs for ~5% of incoming webhooks
  await processStripeEvent(event);

  return Response.json({ processed: true });
}

Move 1 in Action: Don't Run At All

The cheapest function is the one that never executes. Use edge caching to serve responses without touching your function.

app/api/catalog/route.ts
export async function GET() {
  const products = await db.products.findMany({
    where: { status: 'active' },
    include: { category: true },
  });

  return Response.json(products, {
    headers: {
      // Cache at edge for 5 minutes
      'Cache-Control': 'public, s-maxage=300',
      // Serve stale while revalidating for 1 hour
      'CDN-Cache-Control': 'stale-while-revalidate=3600',
    },
  });
}

After the first request, subsequent requests hit the edge cache—zero function invocations, zero cost. Your function runs once every 5 minutes no matter how many people hit the endpoint.

If you get 10,000 requests in those 5 minutes? You pay for 1.

Surgical Cache Invalidation

For cases where edge caching is too blunt, use Next.js's data cache with tags. This lets you cache aggressively but invalidate precisely when data changes.

lib/data.ts
import { unstable_cache } from 'next/cache';

export const getVenueWithAvailability = unstable_cache(
  async (venueId: string, date: string) => {
    const venue = await db.venues.findUnique({
      where: { id: venueId },
      include: {
        slots: {
          where: { date, status: 'available' },
        },
      },
    });
    return venue;
  },
  ['venue-availability'],  // Cache key
  {
    revalidate: 60,        // Auto-refresh every minute
    tags: ['venues'],      // Manual invalidation handle
  }
);

When someone books a slot:

app/api/bookings/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const booking = await createBooking(await request.json());

  // Bust the cache—next request will fetch fresh data
  revalidateTag('venues');

  return Response.json(booking);
}

The Memory Paradox

Here's a counterintuitive truth: higher memory can cost less.

Memory allocation determines how much CPU horsepower your function gets. More memory = faster execution = fewer milliseconds billed. Sometimes doubling the memory halves the execution time—and your net cost drops.

vercel.json
{
  "functions": {
    "app/api/lightweight/**": {
      "memory": 512,
      "maxDuration": 10
    },
    "app/api/reports/**": {
      "memory": 1536,
      "maxDuration": 60
    }
  }
}
Function TypeMemoryWhy
Auth checks, simple CRUD512 MBMinimal work, save allocation
Standard APIs1024 MBGood default balance
Report generation1536 MBAggregation needs headroom
Image/PDF processing2048+ MBHeavy lifting, needs CPU power

Test Both Directions

Don't assume lower memory = lower cost. A function that runs 100ms at 512MB might run 40ms at 1024MB. At scale, the faster version is cheaper. Benchmark your actual workloads.

Seeing What You're Paying For

Add timing headers to your functions during development. Then check DevTools to see exactly where milliseconds go.

export async function GET(request: Request) {
  const start = performance.now();

  // ... your logic ...

  const duration = performance.now() - start;

  return Response.json(data, {
    headers: {
      'Server-Timing': `total;dur=${duration}`,
    },
  });
}

Open DevTools → Network → click the request → Timing tab. The Server-Timing header shows up there. This is your ground truth for Active CPU time.


The Cheat Sheet

When You Want To...Do This
Eliminate costs entirelyCache at edge with s-maxage
Skip 95% of requestsBail early with cheap checks
Cut JS execution in halfPush aggregation to SQL
Halve response time for freePromise.all() independent fetches
Speed up CPU-heavy workIncrease memory allocation
Invalidate cache surgicallyUse unstable_cache with tags

The Summary

The meter only runs when JS runs

I/O wait is free. Database queries, API calls, file reads—all free. Only your JavaScript execution costs money.

Caching is the ultimate move

A cached response = zero invocations = zero cost. Use s-maxage for edge caching, unstable_cache for data-level control.

Order matters

Put cheap checks first. Header validation, permission checks, event filtering—before you touch the database or call external APIs.

Parallelize for UX, not cost

Promise.all() doesn't save money (I/O wait is free anyway). But it halves response time, which users will notice.

Memory is a lever

More memory = more CPU = faster execution. Sometimes increasing memory reduces total cost. Benchmark both directions.


Before optimizing individual functions, make sure you're not running them unnecessarily. See Rendering Strategy for the page-level decisions that determine if functions run at all.

On this page