billing hacks

The Static Default Principle

Every page should be static until proven otherwise. This mindset shift alone can cut costs by 90%.

RENDERING COST LADDERcheaperexpensiveStatic (SSG)~$0 / 100K viewsISR~$0.05 / 100K viewsPure SSR$10-20 / 100K views100-200xmore than static

The Rule

Static until proven otherwise. Use ISR when content updates. Use SSR only when content must be fresh, personalized, AND in the first paint. Most "dynamic" pages fail at least one of these tests.

Here's the uncomfortable truth: most pages that run server-side don't need to.

We default to SSR because it's convenient, not because it's necessary. The page works, so we ship it. But at scale, that convenience costs 100x what it should.

The Cost Gap Is Massive

Let's put real numbers on it:

StrategyCost per 100K viewsWhen You'd Use It
Static (SSG)~$0Content that rarely changes
ISR (time-based)~$0.05Updates every few minutes/hours
ISR (on-demand)~$0.02Updates when you trigger it
SSR + cache headers~$2-5Dynamic but shareable across users
Pure SSR~$10-20Must be fresh AND personalized

Moving from pure SSR to ISR typically cuts costs by 95-99%. That's not optimization—that's a different cost structure entirely.

The Math

A product page with 100K monthly views costs ~15withSSR.WithISR(hourlyrevalidation),itcosts 15 with SSR. With ISR (hourly revalidation), it costs ~0.05. Same data freshness for practical purposes, 300x cheaper.

The Three-Question Test

Before you render a page server-side, ask:

Rendering strategy decision tree showing the three questions: Does content change per-request? Is it personalized? Must it be fresh on first paint?

Question 1: Does the content actually change per-request?

No → Stop. Use Static or ISR. You're done.

Partly → Static shell + client-side fetch for the dynamic bits.

Completely → Continue to Question 2.

Question 2: Is it personalized to the user?

No → Use SSR with cache headers. One render serves thousands of users.

Yes → Continue to Question 3.

Question 3: Does it NEED to be in the first paint?

No → Static shell + client-side fetch. Users see the layout instantly, data loads after.

Yes, absolutely → Fine, use SSR. But revisit this assumption in a few months.

Most pages don't pass all three tests. Most pages can be static or ISR.

The Patterns

Pattern 1: Pure Static

For marketing pages, docs, blog posts—anything that doesn't change between requests:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}
// No dynamic config = static by default

Cost: Zero. You pay at build time, nothing at runtime.

Pattern 2: ISR with a Timer

For content that updates but doesn't need to be real-time:

// app/pricing/page.tsx
export const revalidate = 3600;

export default async function PricingPage() {
  const plans = await getPlans();
  return <PricingTable plans={plans} />;
}

Cost: One function execution per hour. Doesn't matter if you get 100 visits or 100,000—same price.

Pattern 3: ISR with On-Demand Revalidation

For content that changes unpredictably (CMS publishes, inventory updates):

// app/products/[id]/page.tsx
export const dynamic = 'force-static';

export async function generateStaticParams() {
  const products = await getAllProducts();
  return products.map((p) => ({ id: p.id }));
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <ProductDetails product={product} />;
}

Then when the product updates:

// app/api/webhooks/cms/route.ts
import { revalidatePath } from 'next/cache';

export async function POST(request: Request) {
  const { secret, productId } = await request.json();

  if (secret !== process.env.WEBHOOK_SECRET) {
    return Response.json({ error: 'Nope' }, { status: 401 });
  }

  revalidatePath(`/products/${productId}`);
  return Response.json({ done: true });
}

Cost: One function execution per content change. Not per view.

Pattern 4: Static Shell + Client Data

The best of both worlds for dashboards and personalized pages:

// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <DashboardShell>
      <Suspense fallback={<StatsSkeleton />}>
        <UserStats /> {/* Fetches client-side */}
      </Suspense>
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity /> {/* Fetches client-side */}
      </Suspense>
    </DashboardShell>
  );
}
// app/dashboard/user-stats.tsx
'use client';
import useSWR from 'swr';

export function UserStats() {
  const { data, isLoading } = useSWR('/api/user/stats');
  if (isLoading) return <StatsSkeleton />;
  return <StatsDisplay stats={data} />;
}

Cost: Shell is free (static). API calls are minimal—SWR caches locally.

Pattern 5: SSR with Cache Headers (Last Resort)

When you genuinely need server rendering but the result can be shared:

// app/trending/page.tsx
export const dynamic = 'force-dynamic';

export default async function TrendingPage() {
  const trending = await getTrending();
  return <TrendingList items={trending} />;
}
middleware.ts
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/trending') {
    const response = NextResponse.next();
    response.headers.set(
      'Cache-Control',
      'public, s-maxage=60, stale-while-revalidate=300'
    );
    return response;
  }
}

Cost: One render per minute serves everyone. Instead of thousands of renders, you get 60/hour.

How to Audit Your Current State

Run next build and look at the output:

Route (app)                    Size     First Load JS
┌ ○ /                         5.2 kB   89 kB
├ ○ /about                    2.1 kB   86 kB
├ ● /blog/[slug]              3.4 kB   87 kB
├ ƒ /dashboard                8.1 kB   92 kB
├ ƒ /api/user                 0 B      84 kB
└ ○ /pricing                  2.8 kB   87 kB

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML
ƒ  (Dynamic)  server-rendered on demand

Every ƒ is a question mark. Does it need to be dynamic?

The Usual Mistakes

Hidden Dynamic Triggers

These silently force your page into SSR:

// Any of these = SSR
cookies();
headers();
searchParams;
noStore();

The tricky part: these might be deep in your import tree. A utility calls cookies() somewhere, and suddenly your whole page is dynamic.

Fix: Audit your imports. Search for these function calls across your codebase.

The Auth Trap

// This makes the ENTIRE page dynamic
export default async function Page() {
  const session = await getServerSession(); // Calls cookies()!

  return (
    <main>
      <PublicHero />
      <PublicFeatures />
      {session && <UserBadge user={session.user} />}
    </main>
  );
}

You're making 99% of the page dynamic for a tiny user badge. Better:

// Static page with client-side auth
export default function Page() {
  return (
    <main>
      <PublicHero />
      <PublicFeatures />
      <ClientUserBadge /> {/* Checks auth client-side */}
    </main>
  );
}

Over-Using searchParams

// Dynamic because searchParams isn't known at build time
export default function SearchPage({ searchParams }) {
  const query = searchParams.q;
  return <SearchResults query={query} />;
}

But if search happens client-side anyway:

// Static shell, search is client-side
export default function SearchPage() {
  return <SearchInterface />; // Reads URL params in the browser
}

The Savings

When you convert high-traffic SSR pages:

MigrationTypical Savings
Product pages → ISR95-99%
Blog/docs → SSG99%+
Dashboard → static shell60-80%
Listing pages → ISR90-95%

The Summary

Run next build and find the ƒ routes

Each one is either necessary or a cost leak. Question them all.

Default to ISR

Time-based or on-demand. You get fresh content without per-request costs.

Use static shells for personalized pages

Serve the layout instantly, fetch user data client-side. Feels fast, costs little.

Hunt for hidden dynamic triggers

cookies(), headers(), searchParams—audit your imports for these.


When you do need SSR, make it cheaper. See Writing Cheaper Functions for the code-level optimizations.

On this page