Skip to content

Latest commit

 

History

History
496 lines (390 loc) · 16.1 KB

File metadata and controls

496 lines (390 loc) · 16.1 KB

Next.js Caching Guide for cdk-nextjs

This guide explains how cdk-nextjs enables and optimizes the different types of Next.js caching mechanisms using cloud-native AWS services.

Overview

Next.js uses multiple caching layers to improve performance and reduce costs. cdk-nextjs provides cloud-native implementations for these caching mechanisms using S3, DynamoDB, and CloudFront.

Next.js Caching Mechanisms

Next.js uses multiple caching layers, each with a specific CachedRouteKind that determines how cdk-nextjs organizes them in S3:

1. Request Memoization

What: Automatic deduplication of fetch requests with the same URL and options within a single React component tree render. Where: Server (during rendering) Duration: Per-request lifecycle cdk-nextjs Implementation: Handled natively by Next.js - no additional infrastructure needed.

2. Data Cache (FETCH)

What: Persistent cache for data fetched using the fetch() API or third-party libraries. Where: Server Duration: Persistent (can be revalidated) Kind: FETCH cdk-nextjs Implementation:

  • Storage: S3 bucket at /{buildId}/{cache-key}
  • Custom Cache Handler: S3CacheHandler manages read/write operations
  • Revalidation: DynamoDB table tracks tag-based revalidation metadata

3. Full Route Cache

What: Cached HTML and React Server Component (RSC) payloads for statically rendered routes. Where: Server Duration: Persistent (can be revalidated) Kinds:

  • APP_PAGE - App Router pages

  • APP_ROUTE - App Router API routes

  • PAGES - Pages Router pages cdk-nextjs Implementation:

  • ISR Support: Files updated during Incremental Static Regeneration

  • Revalidation: Tag-based invalidation via DynamoDB tracking

4. Image Optimization Cache (IMAGE)

What: Cached optimized images generated by Next.js Image component. Where: Server Duration: Persistent (can be revalidated) Kind: IMAGE cdk-nextjs Implementation:

  • Storage: S3 bucket at /{buildId}/{cache-key}
  • Optimization: Cached resized, format-converted images
  • Revalidation: Time-based or on-demand revalidation

5. Redirect Cache (REDIRECT)

What: Cached redirect configurations for dynamic redirects. Where: Server Duration: Persistent (can be revalidated) Kind: REDIRECT cdk-nextjs Implementation:

  • Storage: S3 bucket at /{buildId}/{cache-key}
  • Configuration: Cached redirect rules and destinations

6. Router Cache (Client-side)

What: Client-side cache of RSC payloads to reduce server requests during navigation. Where: Client browser Duration: User session or time-based cdk-nextjs Implementation: Handled natively by Next.js client - no server infrastructure needed.

Reference Implementations

cdk-nextjs Cache Architecture

S3 Cache Storage

cdk-nextjs uses a dedicated S3 bucket for cache storage with BUILD_ID and kind prefixing for deployment isolation and organization:

Cache Bucket Structure:
/{buildId}/
└── {cache-key}.json             # All cache entries (FETCH, IMAGE, APP_PAGE, APP_ROUTE, PAGES, REDIRECT)

Examples

NKHdPJfH3k5tcfaEY1CVQ/isr/1.json
{
  "lastModified": 1768400373006,
  "value": {
    "kind": "APP_PAGE",
    "html": "<!DOCTYPE html><html lang=\"en\" class=\"[color-scheme:dark]\">...</html>",
    "rscData": {
      "type": "Buffer",
      "data": [
        49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, 109, ...
      ]
    },
    "headers": {
      "x-nextjs-stale-time": "300",
      "x-next-cache-tags": "_N_T_/layout,_N_T_/isr/layout,_N_T_/isr/[id]/layout,_N_T_/isr/[id]/page,_N_T_/isr/1,collection"
    },
    "segmentData": {
      "__type": "Map",
      "data": {
        "/_tree": {
          "type": "Buffer",
          "data": [
            58, 72, 76, 91, 34, 47, 95, 110, 101, 120, 116, 47, 115, 116, 97, ...
          ]
        },
        "/_full": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        },
        "/isr/$d$id/__PAGE__": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        },
        "/isr/$d$id": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        },
        "/isr": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        },
        "/_index": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        },
        "/_head": {
          "type": "Buffer",
          "data": [
            49, 58, 34, 36, 83, 114, 101, 97, 99, 116, 46, 102, 114, 97, 103, ...
          ]
        }
      }
    }
  },
  "tags": []
}
NKHdPJfH3k5tcfaEY1CVQ/favicon-ico.json
{
  "lastModified": 1768399926936,
  "value": {
    "kind": "APP_ROUTE",
    "status": 200,
    "body": {
      "type": "Buffer",
      "data": [
        0, 0, 1, 0, 3, 0, 48, 48, 0, 0, 1, 0, 32, 0, 168, 37, 0, 0, 54, 0, 0, 0, ...
      ]
    },
    "headers": {
      "cache-control": "public, max-age=0, must-revalidate",
      "content-type": "image/x-icon",
      "x-next-cache-tags": "_N_T_/layout,_N_T_/favicon.ico/layout,_N_T_/favicon.ico/route,_N_T_/favicon.ico"
    }
  },
  "tags": []
}
NKHdPJfH3k5tcfaEY1CVQ/7219caec2df443d9c8453d2d63f9893f701fcc35e6a25a9d227652a1860296a1.json
{
  "lastModified": 1768400373089,
  "value": {
    "kind": "FETCH",
    "data": {
      "headers": {
        "access-control-allow-credentials": "true",
        "age": "26672",
        "alt-svc": "h3=\":443\"; ma=86400",
        "cache-control": "max-age=43200",
        "cf-cache-status": "HIT",
        "cf-ray": "9bddc4dbb92cc96f-IAD",
        "connection": "keep-alive",
        "content-encoding": "br",
        "content-type": "application/json; charset=utf-8",
        "date": "Wed, 14 Jan 2026 14:19:33 GMT",
        "etag": "W/\"116-jnDuMpjju89+9j7e0BqkdFsVRjs\"",
        "expires": "-1",
        "nel": "{\"report_to\":\"heroku-nel\",\"response_headers\":[\"Via\"],\"max_age\":3600,\"success_fraction\":0.01,\"failure_fraction\":0.1}",
        "pragma": "no-cache",
        "report-to": "{\"group\":\"heroku-nel\",\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?s=dyvH%2FEDCH%2BYGIXXz6yZUr5CGYyfTVMxjIWIrMfvKg3M%3D\\u0026sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d\\u0026ts=1767999226\"}],\"max_age\":3600}",
        "reporting-endpoints": "heroku-nel=\"https://nel.heroku.com/reports?s=dyvH%2FEDCH%2BYGIXXz6yZUr5CGYyfTVMxjIWIrMfvKg3M%3D&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&ts=1767999226\"",
        "server": "cloudflare",
        "transfer-encoding": "chunked",
        "vary": "Origin, Accept-Encoding",
        "via": "2.0 heroku-router",
        "x-content-type-options": "nosniff",
        "x-powered-by": "Express",
        "x-ratelimit-limit": "1000",
        "x-ratelimit-remaining": "999",
        "x-ratelimit-reset": "1767999231"
      },
      "body": "ewogICJ1c2VySWQiOiAxLAogICJpZCI6IDIsCiAgInRpdGxlIjogInF1aSBlc3QgZXNzZSIsCiAgImJvZHkiOiAiZXN0IHJlcnVtIHRlbXBvcmUgdml0YWVcbnNlcXVpIHNpbnQgbmloaWwgcmVwcmVoZW5kZXJpdCBkb2xvciBiZWF0YWUgZWEgZG9sb3JlcyBuZXF1ZVxuZnVnaWF0IGJsYW5kaXRpaXMgdm9sdXB0YXRlIHBvcnJvIHZlbCBuaWhpbCBtb2xlc3RpYWUgdXQgcmVpY2llbmRpc1xucXVpIGFwZXJpYW0gbm9uIGRlYml0aXMgcG9zc2ltdXMgcXVpIG5lcXVlIG5pc2kgbnVsbGEiCn0=",
      "status": 200,
      "url": "https://jsonplaceholder.typicode.com/posts/2"
    },
    "revalidate": 10
  },
  "tags": ["collection"]
}
NKHdPJfH3k5tcfaEY1CVQ/qRuS9bDf7sJo_E8f0f0HSsPuQf5Dkpu61jOAbF0LuKE.json
{
  "lastModified": 1768399959959,
  "value": {
    "kind": "IMAGE",
    "buffer": {
      "type": "Buffer",
      "data": [
        255, 216, 255, 219, 0, 67, 0, 8, 8, 8, 8, 9, 8, 9, 10, 10, 9, 13, 14, ...
      ]
    },
    "etag": "J29FvqevmUxXsoXBJzPbJC-g_PiMBegRLXAFLl_ZfhE",
    "extension": "jpeg",
    "upstreamEtag": "Vy8iOGRmMC0xOWJiY2Q0MGMyMCI",
    "revalidate": 14400
  },
  "tags": []
}

Key Features:

  • BUILD_ID Isolation: All cache keys prefixed with /{buildId}/
  • Next.js Cache Key Passthrough: Preserves Next.js internal cache key structure
  • Cache Kind Metadata: Cache type stored within each cache entry's metadata

DynamoDB Revalidation Tracking

A DynamoDB table tracks tag-to-S3-key mappings for efficient revalidation:

interface RevalidationItem {
  pk: string; // Partition Key: buildId (e.g., "build-abc123")
  sk: string; // Sort Key: "{tag}#{s3Key}" (e.g., "user-profile#build-abc123/fetch/api-users-123")
  createdAt: number; // Creation timestamp
  revalidatedAt: number; // Last revalidation timestamp
}

interface MetadataItem {
  pk: "METADATA"; // Special partition key for metadata
  sk: "CURRENT_BUILD"; // Special sort key for tracking current build
  buildId: string; // Current BUILD_ID for efficient pruning
  updatedAt: number; // Last update timestamp
}

Example Data:

PK: "build-abc123"    SK: "user-profile#build-abc123/api-users-123"
PK: "build-abc123"    SK: "user-profile#build-abc123/users-profile"
PK: "build-abc123"    SK: "product-data#build-abc123/products-electronics"
PK: "build-abc123"    SK: "product-images#build-abc123/product-123-thumb"
PK: "METADATA"        SK: "CURRENT_BUILD"    buildId: "build-abc123"

Key Features:

  • Efficient Pruning: Query previous build's partition to delete old entries (no table scan)
  • Metadata Tracking: Stores current BUILD_ID for identifying previous build during pruning
  • Full S3 Key Storage: Sort key contains complete S3 path for direct deletion
  • Efficient Revalidation: Query by buildId + tag prefix returns all related cache entries

Custom Cache Handler

The S3CacheHandler implements Next.js's cache interface with comprehensive tag-based revalidation:

export class S3CacheHandler {
  async get(
    cacheKey: string,
    ctx: { kind: CachedRouteKind },
  ): Promise<CacheHandlerValue | null>;

  async set(
    cacheKey: string,
    data: IncrementalCacheValue,
    ctx: { tags: string[] },
  ): Promise<void>;

  async revalidateTag(tag: string): Promise<void>;
}

Cache Entry Structure Examples:

Each cache entry stored in S3 includes both the cached data and associated tags for revalidation checking:

// S3 cache entry structure
{
  "lastModified": 1704067200000,
  "value": {
    "kind": "FETCH",
    "data": {
      "headers": { "content-type": "application/json" },
      "body": "{\"users\": [...]}",
      "url": "https://api.example.com/users"
    },
    "revalidate": 3600
  },
  "tags": ["user-profile", "user-list"]
}

Cache Retrieval Process:

  1. Fetch from S3: Retrieve cache entry with embedded tags
  2. Revalidation Check: Query DynamoDB to check if any tag has been revalidated since cache creation
  3. Timestamp Comparison: Compare revalidatedAt with cache entry's lastModified
  4. Invalidation: If any tag was revalidated after cache creation, delete S3 entry and return cache miss
  5. Return: If valid, return cached data without tags

Key Features:

  • BUILD_ID Isolation: All cache keys prefixed with {buildId}/
  • Tag Storage: Tags stored with cache entries for revalidation checking
  • Timestamp Validation: Prevents serving stale data after tag revalidation
  • Graceful Error Handling: Logs errors and returns cache miss on failures
  • Bulletproof Consistency: Even if S3 deletions fail, stale data won't be served

Static Assets vs Cache Assets

Static Assets (NextjsStaticAssets)

Purpose: Serve unchanging build artifacts via CloudFront CDN Storage: Dedicated S3 bucket Content:

  • public/ folder contents
  • .next/static/ build artifacts (JS, CSS, images)
  • BUILD_ID metadata for pruning

CloudFront Integration:

  • Requests to /_next/static/* → S3 bucket
  • Requests to /public/* → S3 bucket
  • Long-term caching headers for performance

Cache Assets (S3CacheHandler)

Purpose: Store dynamic cache data that changes during runtime Storage: Separate S3 cache bucket Content:

  • Data Cache (fetch responses)
  • Full Route Cache (HTML, RSC payloads)
  • ISR-generated content

Revalidation and Cache Invalidation

On-Demand Revalidation

When revalidateTag("user-profile") is called:

  1. Query DynamoDB: Find all cache keys tagged with pk = {buildId} and sk starts_with user-profile
  2. Update Timestamps: Mark revalidation time in DynamoDB for each cache entry
  3. Delete S3 Objects: Remove corresponding cache files from S3

Revalidation Safety: Even if S3 deletions fail due to network issues or race conditions, the cache handler will detect stale data during the next get() operation by comparing timestamps and automatically remove invalid entries.

Cache Retrieval After Revalidation:

  1. Fetch Cache Entry: Retrieve from S3 with embedded tags
  2. Check Revalidation: Query DynamoDB for each tag's revalidatedAt timestamp
  3. Compare Timestamps: If any revalidatedAt > cache lastModified, cache is invalid
  4. Auto-cleanup: Delete stale S3 entry and return cache miss
  5. Fresh Data: Next request will fetch fresh data and create new cache entry

Time-based Revalidation

For routes with time-based revalidation (e.g., revalidate: 3600):

  • Next.js checks cache age before serving
  • Triggers background regeneration when expired
  • Updates cache files in S3 automatically

Tag-based Revalidation

// In your API route or Server Action
import { revalidateTag } from "next/cache";

export async function updateUser(userId: string) {
  // Update user data
  await updateUserInDatabase(userId);

  // Invalidate all cache entries tagged with this user
  revalidateTag(`user-${userId}`);
  revalidateTag("user-list");
}

Performance Characteristics

S3 Cache Operations

  • Read Latency: ~10-50ms (regional S3)
  • Write Latency: ~20-100ms (with DynamoDB update)
  • Throughput: Scales automatically with demand
  • Cost: Pay-per-request, no minimum charges

DynamoDB Revalidation

  • Query Latency: ~1-5ms (single partition key lookup)
  • Revalidation Check: Additional 1-5ms per tag during cache retrieval
  • Scalability: Handles millions of cache entries
  • Cost: Minimal - only pays for actual reads/writes
  • Consistency: Eventually consistent (sufficient for cache invalidation)
  • Safety: Timestamp-based validation prevents serving stale data

Error Handling

  • S3 Failures: Logs errors and returns cache miss, allowing Next.js to fetch fresh data
  • DynamoDB Failures: Logs errors and continues without revalidation tracking
  • Graceful Degradation: Cache misses trigger fresh data fetching automatically

Troubleshooting

Cache Not Working

  1. Check environment variables are set correctly
  2. Verify S3 bucket and DynamoDB table exist
  3. Check Lambda/container permissions
  4. Review CloudWatch logs for errors

Revalidation Issues

  1. Verify tags are set correctly in fetch requests
  2. Check DynamoDB for tag-to-cache-key mappings
  3. Ensure revalidateTag() calls are working
  4. Monitor S3 object deletions
  5. Check CloudWatch logs for "CACHE INVALIDATED BY TAG" messages
  6. Verify timestamp comparisons in DynamoDB revalidation entries

Performance Problems

  1. Check S3 request latency in CloudWatch
  2. Monitor DynamoDB throttling
  3. Review error logs for persistent failures
  4. Verify regional S3 bucket placement

This comprehensive caching system provides the performance benefits of Next.js caching while leveraging AWS's scalable, cost-effective infrastructure. For information about automatic cleanup of old cache data, see the pruning guide.