# 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

- [OpenNext](https://github.com/opennextjs/opennextjs-aws/blob/1aa300d33601e2fe7b5a289988fa0a32d727d26a/packages/open-next/src/adapters/cache.ts)
- Next.js' [FileSystemCache](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/incremental-cache/file-system-cache.ts)

## 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

<details>

<summary>NKHdPJfH3k5tcfaEY1CVQ/isr/1.json</summary>

```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": []
}
```

</details>

<details>

<summary>NKHdPJfH3k5tcfaEY1CVQ/favicon-ico.json</summary>

```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": []
}
```

</details>

<details>

<summary>NKHdPJfH3k5tcfaEY1CVQ/7219caec2df443d9c8453d2d63f9893f701fcc35e6a25a9d227652a1860296a1.json</summary>

```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"]
}
```

</details>

<details>

<summary>NKHdPJfH3k5tcfaEY1CVQ/qRuS9bDf7sJo_E8f0f0HSsPuQf5Dkpu61jOAbF0LuKE.json</summary>

```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": []
}

```

</details>

**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:

```typescript
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:

```typescript
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:

```json
// 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

```typescript
// 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](./pruning-guide.md).
