Vercel Logo

Smart caching

Refresh /mower in dev and keep an eye on the AI Gateway dashboard. Every refresh asks Claude to summarize the same Mower3000 reviews again, plus one more call for the insights. That's about $0.004 per page view (an example rate for this prompt) to learn what we already knew a second ago.

The reviews haven't changed since the last refresh, so the summary shouldn't either. Let's cache both results so the model runs once and later visitors get the saved answer.

Outcome

Cache AI-generated summaries and insights with the Next.js "use cache" directive so repeat page views reuse the result instead of calling the model again.

Fast Track

  1. Enable cacheComponents in next.config.ts
  2. Add "use cache" directive to AI functions with cacheLife("hours")
  3. Add cacheTag for on-demand invalidation
  4. Render <AIReviewSummary> on the product page instead of <StreamingSummary>
  5. Test first load (generates) vs subsequent loads (cached)

Hands-on Exercise 3.1

Add caching to AI functions to improve performance and reduce costs:

Requirements:

  1. Enable Cache Components in next.config.ts
  2. Add "use cache" directive to summarizeReviews
  3. Add "use cache" directive to getReviewInsights
  4. Use cacheLife("hours") for 1-hour cache duration
  5. Add cacheTag for targeted cache invalidation
  6. Render the cached AIReviewSummary component on the product page
  7. Verify caching behavior in development

Implementation hints:

  • "use cache" goes at the top of the function body
  • cacheLife accepts built-in profiles: "seconds", "minutes", "hours", "days", "weeks", "max"
  • cacheTag enables on-demand invalidation with revalidateTag()
  • First request generates, subsequent requests are instant

Understanding Next.js 16 Caching

Next.js 16 provides the "use cache" directive for declarative caching:

import { cacheLife, cacheTag } from "next/cache";
 
export async function getData(id: string) {
  "use cache";
  cacheLife("hours");
  cacheTag(`data-${id}`);
 
  // Expensive operation - only runs on cache miss
  return await fetchData(id);
}

How it works:

  1. First call: Executes function, caches result
  2. Subsequent calls: Returns cached result instantly
  3. After cache lifetime expires: Background regeneration on next request
  4. Manual invalidation: Use revalidateTag() to clear specific caches

Benefits:

  • Instant response times (no AI call)
  • Reduced API costs (only regenerate periodically)
  • Fresh content (automatic revalidation)
  • Simple syntax (just add the directive)

Step 1: Enable Cache Components

Update next.config.ts (or create it):

next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  cacheComponents: true,
};
 
export default nextConfig;

This enables the "use cache" directive across your application.

Step 2: Cache Summary Function

Update lib/ai-summary.ts:

lib/ai-summary.ts
import { generateText, Output } from "ai";
import { cacheLife, cacheTag } from "next/cache";
import { Product, ReviewInsights, ReviewInsightsSchema } from "./types";
 
export async function summarizeReviews(product: Product): Promise<string> {
  "use cache";
  cacheLife("hours");
  cacheTag(`product-summary-${product.slug}`);
 
  const averageRating =
    product.reviews.reduce((acc, review) => acc + review.stars, 0) /
    product.reviews.length;
 
  const prompt = `Write a summary of the reviews for the ${
    product.name
  } product. The product's average rating is ${averageRating} out of 5 stars.
 
Your goal is to highlight the most common themes and sentiments expressed by customers.
If multiple themes are present, try to capture the most important ones.
If no patterns emerge but there is a shared sentiment, capture that instead.
Try to use natural language and keep the summary concise.
Use a maximum of 4 sentences and 30 words.
Don't include any word count or character count.
No need to reference which reviews you're summarizing.
Do not reference the star rating in the summary.
 
Start the summary with "Customers like…" or "Customers mention…"
 
Here are 3 examples of good summaries:
Example 1: Customers like the quality, space, fit and value of the sport equipment bag case. They mention it's heavy duty, has lots of space and pockets, and can fit all their gear. They also appreciate the portability and appearance. That said, some disagree on the zipper.
Example 2: Customers like the quality, ease of installation, and value of the transport rack. They mention that it holds on to everything really well, and is reliable. Some complain about the wind noise, saying it makes a whistling noise at high speeds. Opinions are mixed on fit, and performance.
Example 3: Customers like the quality and value of the insulated water bottle. They say it keeps drinks cold for hours and the lid seals well. Some customers have different opinions on size and durability.
 
Hit the following tone based on rating:
- 1-2 stars: negative
- 3 stars: neutral
- 4-5 stars: positive
 
The customer reviews to summarize are as follows:
${product.reviews
    .map((review, i) => `Review ${i + 1}:\n${review.review}`)
    .join("\n\n")}`;
 
  try {
    const { text } = await generateText({
      model: "anthropic/claude-sonnet-4.5",
      prompt,
      maxOutputTokens: 1000,
      temperature: 0.75,
    });
 
    // Clean up the response
    return text
      .trim()
      .replace(/^"/, "")
      .replace(/"$/, "")
      .replace(/[\[\(]\d+ words[\]\)]/g, "");
  } catch (error) {
    console.error("Failed to generate summary:", error);
    throw new Error("Unable to generate review summary. Please try again.");
  }
}

What changed:

  • Added "use cache" directive at function start
  • Added cacheLife("hours") for 1-hour cache duration
  • Added cacheTag with product slug for targeted invalidation

Step 3: Cache Insights Function

Update the getReviewInsights function in the same file:

lib/ai-summary.ts
export async function getReviewInsights(
  product: Product
): Promise<ReviewInsights> {
  "use cache";
  cacheLife("hours");
  cacheTag(`product-insights-${product.slug}`);
 
  const averageRating =
    product.reviews.reduce((acc, review) => acc + review.stars, 0) /
    product.reviews.length;
 
  const prompt = `Analyze the following customer reviews for the ${product.name} product (average rating: ${averageRating}/5).
 
Extract:
1. Pros: 3-5 positive aspects customers appreciate
2. Cons: 3-5 negative aspects or concerns mentioned
3. Themes: 3-5 key themes that emerge across reviews
 
Be specific and concise. Each item should be 3-7 words.
 
Reviews:
${product.reviews
    .map(
      (review, i) => `Review ${i + 1} (${review.stars} stars):\n${review.review}`
    )
    .join("\n\n")}`;
 
  try {
    const { output } = await generateText({
      model: "anthropic/claude-sonnet-4.5",
      output: Output.object({ schema: ReviewInsightsSchema }),
      prompt,
    });
 
    return output;
  } catch (error) {
    console.error("Failed to extract insights:", error);
    throw new Error("Unable to extract review insights. Please try again.");
  }
}

Both functions now:

  • Cache results for 1 hour (cacheLife("hours"))
  • Use product-specific tags for invalidation
  • Return instantly on subsequent requests
  • Automatically revalidate in background

Step 4: Render the cached summary

There's a catch. summarizeReviews is cached now, but the product page doesn't call it. Since 2.4, the summary card is <StreamingSummary>, which fetches /api/summary/[slug], and that route calls streamReviewSummary on every visit. Nothing in that path runs inside a "use cache" function, so caching summarizeReviews saves us exactly zero model calls.

Streaming was great for showing words as they arrive. Once the summary is cached, there's nothing left to wait for, so let's put the AIReviewSummary component from 2.2 back on the page. It awaits the cached summarizeReviews:

app/[productId]/page.tsx
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
import { AIReviewSummary } from "@/components/ai-review-summary";
import { ReviewInsights } from "@/components/review-insights";
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;
 
  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>
 
        <AIReviewSummary product={product} />
        <ReviewInsights product={product} />
 
        <Reviews product={product} />
      </div>
    </main>
  );
}
 
// ... (generateStaticParams and generateMetadata remain the same)

Both AI cards now read from cached functions. You can leave components/streaming-summary.tsx and the API route in place. Nothing renders them anymore, and the next lesson adds error handling to AIReviewSummary.

Try It

  1. Clear any existing cache and restart:

    rm -rf .next
    pnpm dev
  2. Visit a product page:

    http://localhost:3000/mower
    
  3. Check terminal output (first load):

    GET /mower 200 in 4.2s
    

    Timing:

    • Summary generation: ~2s
    • Insights generation: ~2s
    • Total: ~4s
    • Cost: ~$0.004
  4. Refresh the page (cached load):

    GET /mower 200 in 52ms
    

    Timing:

    • Summary: instant (cached)
    • Insights: instant (cached)
    • Total: ~50ms
    • Cost: $0.00

    That's 98.7% faster and free!

  5. Try different products:

    • /ecoBright - First load: ~4s, subsequent: ~50ms
    • /aquaHeat - First load: ~4s, subsequent: ~50ms

    Each product has its own cache entry.

  6. Deploy and refresh (production):

    After you commit and push (see Commit below) and the deployment is ready, open your production /mower and refresh it a few times. The request count in AI Gateway doesn't move. The AI calls for all three products ran during the build, so the build is the one place you'll see them.

Cache Invalidation

Manually invalidate cache when reviews change:

app/actions.ts
"use server";
 
import { revalidateTag } from "next/cache";
 
export async function invalidateProductCache(productSlug: string) {
  revalidateTag(`product-summary-${productSlug}`, "max");
  revalidateTag(`product-insights-${productSlug}`, "max");
}

Use cases:

  • New review submitted → invalidate that product's cache
  • Product updated → invalidate cache
  • Admin triggers refresh → invalidate cache

Built-in Cache Profiles

Next.js provides several built-in cacheLife profiles:

ProfileStale (client)Revalidate (server)Expire
"seconds"30s1s60s
"minutes"5m1m1h
"hours"5m1h1d
"days"5m1d1w
"weeks"5m1w30d
"max"5m30d1y

For AI summaries: "hours" is a good default—fresh enough for new reviews, cached enough to save costs.

Performance Comparison

Before caching:

Page Load:
├─ Generate summary: 2.1s ($0.002)
├─ Generate insights: 2.0s ($0.002)
└─ Total: 4.1s ($0.004)

10,000 page views/month:
└─ Cost: $40.00

After caching on Vercel (cacheLife("hours"), 3 products):

Each deployment (build prerenders every product):
└─ 3 products × $0.004 = $0.012

Each hour, per product (one background regeneration):
└─ Up to 3 × 24 × 30 = 2,160 regenerations × $0.004 = up to $8.64

10,000 page views/month:
└─ Total: up to ~$8.64, plus $0.012 per deploy

The cached number stops depending on page views. It depends on how many products you have and how often the cache revalidates. Double the traffic and the bill stays put. Switch to cacheLife("days") and the hourly line drops to 90 regenerations a month, which is plenty for reviews that change a few times a week.

Benefits:

  • Model calls scale with products and revalidations, not visitors
  • Same UX (users see instant results)
  • Fresh data (revalidates hourly)

Production Deployment

The product pages from 1.4 are prerendered at build time. With Cache Components on, summarizeReviews and getReviewInsights run during the build, and their results land in each page's static shell. Visitors get that prerendered page from Vercel's CDN, so the model doesn't run per visit.

  1. During the build → both AI calls run once per product
  2. Every visitor gets the prerendered page (~50ms)
  3. After 1 hour → the next request still gets the cached page while Vercel regenerates it in the background, which calls the model again
  4. Users always get instant responses (cached or fresh)

Two details trip people up here.

The default "use cache" handler keeps entries in memory. On Vercel Functions, each request can land on a different instance, so a runtime cache entry saved by one request usually isn't there for the next. The prerendered page is what visitors share. That's also why the hourly regeneration calls the model again instead of finding a saved result.

A new deployment starts with an empty cache. The cache key includes the build ID, so every deploy runs the AI calls again during the build. Push ten typo fixes in an afternoon and you've paid for ten rounds of mower summaries. This is expected, and at $0.012 a deploy it's a cheap habit.

If you render AI output at request time instead (after reading cookies() or searchParams, for example), there's no prerendered page to share, and in-memory entries rarely survive between requests. For that case Next.js has "use cache: remote", which stores entries in a remote cache shared by every instance. Each lookup costs a network round trip plus storage and platform fees, so save it for cache misses that cost more than the lookup. Our product pages don't need it.

Commit

git add next.config.ts lib/ai-summary.ts app/\[productId\]/page.tsx
git commit -m "feat(ai): add caching to reduce costs and improve performance"
git push

Done-When

  • cacheComponents enabled in next.config.ts
  • summarizeReviews uses "use cache" directive
  • getReviewInsights uses "use cache" directive
  • Cache tags added for both functions
  • Cache lifetime set to 1 hour with cacheLife("hours")
  • Product page renders <AIReviewSummary> instead of <StreamingSummary>
  • Verified first load generates content
  • Verified subsequent loads return instantly
  • Cache invalidation function created
  • Refreshing the deployed /mower page adds no new requests in AI Gateway

What's next

You've built a complete AI-powered review summary feature with caching for cost reduction. Next, you'll prepare for production by handling AI failures gracefully and configuring model fallbacks.


Sources:

Was this helpful?

supported.