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:
Duration | Use Case | Examples |
---|---|---|
60s | Real-time data | Stock prices, live scores, availability status |
300s (5min) | Standard content | Product listings, blog posts, search results |
3600s (1hr) | Stable content | Documentation, marketing pages, legal terms |
86400s (24hr) | Static assets | Logos, 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:
- Serves cached content immediately (even if stale)
- Triggers background revalidation
- Updates cache with fresh data
- 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: