StarterApp Docs
Performance

Caching Public Data

Accelerating API responses through edge distribution

Pro Tip

Edge caching serves content from CDN locations near users. Use securePublicJson() with maxAge to enable caching while maintaining security headers.

Edge Caching Strategy

Edge caching distributes content globally through CDN nodes. Users receive responses from nearby locations within milliseconds. Origin servers handle fewer requests, reducing load and costs.

Security-First Caching Helpers

The framework provides @workspace/security helpers that enforce correct caching behavior:

import { securePublicJson } from '@workspace/security';

export async function GET() {
  const products = await fetchProducts();

  return securePublicJson(products, { maxAge: 300 });
}

securePublicJson adds:

  • Cache-Control: public, max-age=300
  • Vary: Accept-Encoding
  • Security headers (CSP, X-Content-Type-Options)
  • Proper content type
import { secureUserJson } from '@workspace/security';

export async function GET(req: NextRequest) {
  const session = await getCurrentSession();
  const userData = await fetchUserProfile(session.userId);

  return secureUserJson(userData);
}

secureUserJson prevents caching:

  • Cache-Control: private, no-cache, no-store, must-revalidate
  • Vary: Cookie for session-based responses
  • Security headers
  • Never caches user-specific data
import { secureErrorJson } from '@workspace/security';

export async function GET() {
  try {
    const data = await riskyOperation();
    return securePublicJson(data, { maxAge: 60 });
  } catch (error) {
    return secureErrorJson(
      { message: 'Operation failed', code: 'SERVER_ERROR' },
      { status: 500 }
    );
  }
}

secureErrorJson prevents error caching:

  • No cache headers on error responses
  • Consistent error shape
  • Security headers

Never cache user-specific data

Using securePublicJson for user data exposes private information to other users. Always use secureUserJson for authenticated responses.

Real Implementation Example

The support ticket API demonstrates proper caching discipline:

import { secureUserJson, secureErrorJson } from '@workspace/security';

export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

export async function POST(req: NextRequest) {
  try {
    await requireSession();
    const body = await parseRequestBody(req);
    const token = await getConvexTokenOrThrow();

    const ticketId = await fetchMutation(
      api.support.createSupportTicket,
      body,
      { token }
    );

    return secureUserJson(
      { success: true, ticketId },
      { status: 201 }
    );
  } catch (error) {
    return secureErrorJson(
      { message: error.message, code: error.code },
      { status: getHttpStatus(error) }
    );
  }
}

This route:

Disables static optimization

revalidate = 0 and dynamic = "force-dynamic" ensure fresh data on every request.

Uses user-scoped responses

secureUserJson prevents caching of ticket data that's specific to the authenticated user.

Handles errors without caching

secureErrorJson ensures error responses don't get cached at edge locations.

Choosing Cache Durations

Different content types require different maxAge values:

DurationUse CaseExamples
60sReal-time dataStock prices, live scores, availability status
300s (5min)Standard contentProduct listings, blog posts, search results
3600s (1hr)Stable contentDocumentation, marketing pages, legal terms
86400s (24hr)Static assetsLogos, icons, generated images (with versioned URLs)

Start conservative

Begin with shorter durations (60-300s). Increase gradually as you verify data freshness requirements. Shortening cache duration is easier than dealing with stale data issues.

Identifying Cacheable Content

Public data that all users see identically benefits from edge caching:

User-specific data must never cache publicly

These require secureUserJson:

  • User profiles and preferences
  • Shopping carts and wishlists
  • Private messages or notifications
  • Account settings and billing
  • Personalized recommendations

Stale-While-Revalidate Pattern

Advanced caching strategies serve cached content while updating in the background:

export async function GET() {
  const products = await fetchProducts();

  return securePublicJson(products, {
    maxAge: 300,        // Fresh for 5 minutes
    swr: 3600          // Serve stale for 1 hour while revalidating
  });
}

This pattern:

  1. Serves cached content immediately (even if stale)
  2. Triggers background revalidation
  3. Updates cache with fresh data
  4. Next request receives updated content

Users experience instant responses while data stays reasonably fresh.

Page-Level Caching with Revalidation

Next.js provides page-level caching through route segment config:

export const revalidate = 600; // Regenerate static HTML every 10 minutes

export default async function HomePage() {
  return <HeroSection />;
}

This configuration:

  • Generates static HTML at build time
  • Serves from edge with instant response times
  • Regenerates every 10 minutes to keep content fresh
  • Eliminates server requests for cached pages

The marketing site uses this pattern extensively. Static pages serve instantly while periodic regeneration ensures content freshness.

Combine with API caching

Page-level caching works best when underlying API routes also cache appropriately. This creates multiple layers of caching efficiency.

Monitoring Cache Effectiveness

Track cache performance through CDN analytics and response headers:

Check Cache-Control headers

Verify responses include correct caching directives:

curl -I https://your-app.com/api/products

Cache-Control: public, max-age=300
Vary: Accept-Encoding

Monitor cache hit rates

CDN providers report hit/miss ratios. Target 80-95% hit rates for public content.

Measure origin load reduction

Compare origin requests before and after implementing caching. Expect 80-90% reduction.

Common Caching Mistakes

Avoid these anti-patterns

These patterns compromise security or performance:

Next Steps