This guide explains how cdk-nextjs enables and optimizes the different types of Next.js caching mechanisms using cloud-native AWS services.
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 uses multiple caching layers, each with a specific CachedRouteKind that determines how cdk-nextjs organizes them in S3:
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.
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
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
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
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
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.
- OpenNext
- Next.js' FileSystemCache
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)
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
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
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:
- Fetch from S3: Retrieve cache entry with embedded tags
- Revalidation Check: Query DynamoDB to check if any tag has been revalidated since cache creation
- Timestamp Comparison: Compare
revalidatedAtwith cache entry'slastModified - Invalidation: If any tag was revalidated after cache creation, delete S3 entry and return cache miss
- 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
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
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
When revalidateTag("user-profile") is called:
- Query DynamoDB: Find all cache keys tagged with
pk = {buildId} and sk starts_with user-profile - Update Timestamps: Mark revalidation time in DynamoDB for each cache entry
- 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:
- Fetch Cache Entry: Retrieve from S3 with embedded tags
- Check Revalidation: Query DynamoDB for each tag's
revalidatedAttimestamp - Compare Timestamps: If any
revalidatedAt> cachelastModified, cache is invalid - Auto-cleanup: Delete stale S3 entry and return cache miss
- Fresh Data: Next request will fetch fresh data and create new cache entry
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
// 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");
}- 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
- 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
- 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
- Check environment variables are set correctly
- Verify S3 bucket and DynamoDB table exist
- Check Lambda/container permissions
- Review CloudWatch logs for errors
- Verify tags are set correctly in fetch requests
- Check DynamoDB for tag-to-cache-key mappings
- Ensure revalidateTag() calls are working
- Monitor S3 object deletions
- Check CloudWatch logs for "CACHE INVALIDATED BY TAG" messages
- Verify timestamp comparisons in DynamoDB revalidation entries
- Check S3 request latency in CloudWatch
- Monitor DynamoDB throttling
- Review error logs for persistent failures
- 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.