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 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:
| Move | What It Means | Typical Savings |
|---|---|---|
| 1. Don't run at all | Cache the response | 100% (literally free) |
| 2. Bail early | Exit before expensive work | 30-70% |
| 3. Offload to DB | Let SQL do the math | 40-60% |
| 4. Shrink the work | Return less data | 20-40% |
| 5. Parallelize | Same work, faster UX | 0% savings, 2-4x faster |

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 }} />;
}| Metric | Value |
|---|---|
| Wall clock | 800ms (200ms × 4) |
| Active CPU | ~40ms |
| Cost | $X |
| User experience | Sluggish |
// 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 }} />;
}| Metric | Value |
|---|---|
| Wall clock | 400ms (200ms + 200ms) |
| Active CPU | ~40ms |
| Cost | $X (same!) |
| User experience | Snappy |
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:
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.
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.
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:
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.
{
"functions": {
"app/api/lightweight/**": {
"memory": 512,
"maxDuration": 10
},
"app/api/reports/**": {
"memory": 1536,
"maxDuration": 60
}
}
}| Function Type | Memory | Why |
|---|---|---|
| Auth checks, simple CRUD | 512 MB | Minimal work, save allocation |
| Standard APIs | 1024 MB | Good default balance |
| Report generation | 1536 MB | Aggregation needs headroom |
| Image/PDF processing | 2048+ MB | Heavy 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 entirely | Cache at edge with s-maxage |
| Skip 95% of requests | Bail early with cheap checks |
| Cut JS execution in half | Push aggregation to SQL |
| Halve response time for free | Promise.all() independent fetches |
| Speed up CPU-heavy work | Increase memory allocation |
| Invalidate cache surgically | Use 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.