Skip to content

hongjs/nextjs-custom-cache

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

36 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Next.js Custom Cache Handler with Redis - Multi-Pod EKS Ready

A production-ready Next.js application demonstrating distributed caching with Redis, featuring pod hostname visibility and resilient fallback mechanisms for multi-pod Kubernetes deployments on Amazon EKS.

image

Features

Core Caching Features

  • Custom Cache Handlers: Two different implementations
    • Custom Redis cache handler with fallback to in-memory caching
    • @neshca/cache-handler integration with Redis Stack
  • Multiple Rendering Strategies: Live examples of different Next.js caching patterns
  • App Router & Pages Router: Examples for both routing paradigms
  • Centralized Caching: Redis-backed cache shared across all server instances
  • Graceful Fallback: Automatic fallback to LRU in-memory caching when Redis is unavailable
  • Real-world Integration: Connected to JSONPlaceholder API for realistic data fetching scenarios

Multi-Pod Features

  • Pod Hostname Display: Shows which pod is serving each request
  • Cache Handler Logging: Detailed logging for Redis and LRU cache operations
  • Connection Timeout Protection: 5-second timeout prevents hanging when Redis is unavailable
  • LRU Memory Limits: Configurable memory constraints for in-memory cache
  • Kubernetes Ready: Complete K8s manifests for EKS deployment with 3-pod setup
  • Load Balancer Support: Service configuration for distributing traffic across pods

What This Demonstrates

Caching Strategies

  1. Dynamic Rendering (No Cache) - cache: 'no-store'

    • Fresh data on every request
    • Perfect for real-time, user-specific content
    • Example: / (Home page showing JSON data)
  2. ISR (Incremental Static Regeneration) - revalidate: 300

    • Time-based revalidation (60 seconds)
    • Balances freshness with performance
    • Stale-while-revalidate pattern
    • Examples: /app-isr, /app-isr/[id], /page-static, /page-static/[id]
  3. SSG (Static Site Generation)

    • Pre-rendered at build time using generateStaticParams
    • Maximum performance
    • Ideal for rarely changing content
    • Examples: /app-ssg, /app-ssg/[id]
  4. SSR (Server-Side Rendering)

    • Rendered on each request with getServerSideProps
    • Always fresh data, no static caching
    • Examples: /page-server, /page-server/[id]

Cache Handler Implementations

1. Custom Redis Handler (cache-handler.js)

  • Built from scratch using ioredis
  • Direct control over caching logic
  • Implements get, set, and revalidateTag methods
  • Automatic fallback to Map-based in-memory cache
  • Connection status monitoring
  • Wait-until-ready pattern for initialization

2. Neshca Cache Handler (cache-handler-neshca.js) ⭐ Recommended

  • Uses @neshca/cache-handler library
  • Redis Stack integration with 5-second connection timeout
  • LRU (Least Recently Used) fallback handler with configurable memory limits
  • Production-ready with robust error handling
  • Skips Redis during build phase
  • Cache operation logging for debugging in multi-pod environments
  • Null handler filtering prevents crashes when Redis is unavailable

Prerequisites

  • Node.js 18+
  • Docker (for local Redis testing)
  • Yarn or npm
  • (Optional) AWS CLI and kubectl for EKS deployment

Installation

Local Development

  1. Clone the repository:
git clone <repository-url>
cd nextjs-custom-cache
  1. Install dependencies:
yarn install
# or
npm install
  1. (Optional) Start Redis using Docker Compose:
docker-compose up -d redis

Or start Redis separately:

docker run -d -p 6379:6379 redis:7-alpine
# or use Redis Stack for advanced features
docker run -d -p 6379:6379 redis/redis-stack-server:latest
  1. (Optional) Create .env.local file in the project root:
# Redis Configuration for Centralized Caching
# Format: redis://[:password@]host[:port][/db-number]
REDIS_URL=redis://localhost:6379

Usage

Development Mode

yarn dev
# or
npm run dev

Visit http://localhost:3000 to see the application. The pod hostname will be displayed at the top of every page.

Exploring Different Routes

The application includes multiple examples demonstrating different caching strategies:

App Router (Next.js 13+):

  • / - Dynamic rendering (no cache) with JSON viewer
  • /app-isr - ISR list page (revalidate: 300s)
  • /app-isr/1 - ISR detail page for photo #1
  • /app-ssg - SSG list page (built at build time)
  • /app-ssg/1 - SSG detail page with generateStaticParams
  • /gallery - Image gallery testing Next.js Image optimization and Buffer handling

Pages Router (Next.js 12):

  • /page-server - SSR list page with getServerSideProps
  • /page-server/1 - SSR detail page
  • /page-static - ISR list page with getStaticProps
  • /page-static/1 - ISR detail page with getStaticPaths

API Routes:

  • /api/cached-fetch - API with cached fetch() calls (Data Cache testing)
  • /api/real-time - Force-dynamic API (always fresh, never cached)
  • /api/revalidate - On-demand revalidation API (path & tag-based, works with both routers)
  • /api/revalidate-pages - Pages Router native revalidation (res.revalidate method)
  • /api/cache-stats - Cache statistics and Redis monitoring

Testing & Tools:

  • /admin - Interactive cache revalidation UI (purge by path or tags)
  • /stats - Real-time cache statistics and monitoring dashboard

Production Mode

# Build the application
yarn build

# Start the production server
yarn start

Kubernetes Deployment on EKS

Complete instructions for deploying to Amazon EKS with multiple pods are available in k8s/README.md.

Quick Start

  1. Build and push Docker image to ECR:
# Build
docker build -t nextjs-app .

# Tag for ECR
docker tag nextjs-app:latest <account-id>.dkr.ecr.<region>.amazonaws.com/nextjs-app:latest

# Login to ECR
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com

# Push
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/nextjs-app:latest
  1. Update image URL in k8s/deployment.yaml

  2. Deploy to Kubernetes:

# Deploy Redis
kubectl apply -f k8s/redis-deployment.yaml

# Deploy Next.js app (3 replicas by default)
kubectl apply -f k8s/deployment.yaml
  1. Access the application:
# Get LoadBalancer URL
kubectl get svc nextjs-service -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'

Each refresh will show a different pod hostname, demonstrating load balancing across multiple pods!

Scaling

# Scale to 5 pods
kubectl scale deployment nextjs-app --replicas=5

# Verify
kubectl get pods -l app=nextjs-app

Project Structure

.
β”œβ”€β”€ app/                          # App Router pages
β”‚   β”œβ”€β”€ layout.tsx                # Root layout with pod hostname display
β”‚   β”œβ”€β”€ page.tsx                  # Dynamic rendering (no cache)
β”‚   β”œβ”€β”€ app-isr/                  # ISR examples (time-based revalidation)
β”‚   β”œβ”€β”€ app-ssg/                  # SSG examples (build-time static)
β”‚   β”œβ”€β”€ gallery/                  # Image gallery (Buffer/Image optimization test)
β”‚   β”œβ”€β”€ admin/                    # Interactive cache revalidation UI
β”‚   β”œβ”€β”€ stats/                    # Cache statistics dashboard
β”‚   └── api/
β”‚       β”œβ”€β”€ cached-fetch/         # API with cached fetch() - Data Cache test
β”‚       β”œβ”€β”€ real-time/            # Force-dynamic API - No cache test
β”‚       β”œβ”€β”€ revalidate/           # On-demand revalidation API
β”‚       └── cache-stats/          # Cache statistics API
β”œβ”€β”€ pages/                        # Pages Router examples
β”‚   β”œβ”€β”€ page-server/              # SSR examples (getServerSideProps)
β”‚   └── page-static/              # ISR examples (getStaticProps)
β”œβ”€β”€ components/                   # Reusable components
β”‚   β”œβ”€β”€ AppLayout.tsx             # Main layout with navigation
β”‚   β”œβ”€β”€ NavigationMenu.tsx        # Side navigation (updated with new routes)
β”‚   β”œβ”€β”€ PodHostname.tsx           # Pod hostname banner component
β”‚   β”œβ”€β”€ PageHeader.tsx            # Page header with cache info
β”‚   β”œβ”€β”€ ItemCard.tsx              # Photo card component
β”‚   └── ItemDetail.tsx            # Photo detail view
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ api.ts                    # JSONPlaceholder API integration
β”‚   β”œβ”€β”€ redis.ts                  # Redis client utilities
β”‚   └── hostname.ts               # Pod hostname utility
β”œβ”€β”€ k8s/                          # Kubernetes manifests
β”‚   β”œβ”€β”€ deployment.yaml           # App deployment (3 replicas)
β”‚   β”œβ”€β”€ redis-deployment.yaml    # Redis deployment
β”‚   └── README.md                 # Detailed K8s deployment guide
β”œβ”€β”€ cache-handler.js              # Custom Redis cache handler (v1)
β”œβ”€β”€ cache-handler-v2.js           # Custom handler v2
β”œβ”€β”€ cache-handler-v3.js           # Custom handler v3
β”œβ”€β”€ cache-handler-v4.js           # Custom handler v4 (with Gzip compression)
β”œβ”€β”€ cache-handler-neshca.js       # Neshca cache handler (recommended)
β”œβ”€β”€ next.config.ts                # Next.js configuration
β”œβ”€β”€ Dockerfile                    # Production Docker image
β”œβ”€β”€ docker-compose.yml            # Local development setup
β”œβ”€β”€ TESTING.md                    # Comprehensive testing guide & checklist
└── test-cache.sh                 # Automated test script (bash)

Configuration

Switching Cache Handlers

Edit next.config.ts to switch between cache handler implementations:

// Option 1: Custom Redis Handler
const nextConfig: NextConfig = {
  cacheHandler: path.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0,
};

// Option 2: Neshca Cache Handler (Recommended)
const nextConfig: NextConfig = {
  cacheHandler: path.resolve('./cache-handler-neshca.js'),
  cacheMaxMemorySize: 0,
};

LRU Memory Configuration

Adjust LRU cache limits in cache-handler-neshca.js:

const baseLRUHandler = createLruHandler({
  maxItemsNumber: 1000,              // Maximum cached items
  maxItemSizeBytes: 1024 * 1024 * 100, // 100 MB per item
});

Recommended settings by environment:

Environment maxItemsNumber maxItemSizeBytes Total Max Memory
Development 500 50 MB ~25 GB
Small App 1000 100 MB ~100 GB (current)
High Traffic 5000 200 MB ~1 TB
Memory-constrained 200 10 MB ~2 GB

Redis Configuration

The cache handlers support multiple environment variable names:

  • REDIS_URL - Primary Redis connection string
  • KV_URL - Alternative for compatibility with hosting providers (Vercel KV, etc.)

Connection string format:

redis://[:password@]host[:port][/db-number]

Examples:

# Local Redis without auth
REDIS_URL=redis://localhost:6379

# Redis with password
REDIS_URL=redis://:mypassword@localhost:6379

# Kubernetes Redis service
REDIS_URL=redis://redis-service:6379

# Remote Redis with database selection
REDIS_URL=redis://:password@redis-host.com:6379/0

Cache Handler Deep Dive

Neshca Handler Features (cache-handler-neshca.js) ⭐

Enhanced Features:

  • Connection Timeout: 5-second timeout prevents hanging
  • Null Handler Filtering: Automatically filters out failed Redis handler
  • Cache Operation Logging: Detailed logs for debugging multi-pod setups
  • LRU Memory Limits: Configurable memory constraints
  • Build-time Safety: Skips Redis during next build
  • Multi-layer Caching: Redis (primary) β†’ LRU (fallback)
  • Graceful Degradation: Continues working when Redis is down

Logging Examples:

When Redis is available:

Connecting Redis client...
Redis client connected.
Cache handlers configured: [0] Redis Handler, [1] LRU Handler (in-memory)
[Cache] Redis SET: ddaa617fe5992c9b...
[Cache] Redis HIT: ddaa617fe5992c9b...

When Redis is unavailable:

Connecting Redis client...
Failed to connect Redis client: Redis connection timeout
Disconnecting the Redis client...
Redis client disconnected.
Falling back to LRU handler because Redis client is not available.
Cache handlers configured: [0] LRU Handler (in-memory)
[Cache] LRU SET: ddaa617fe5992c9b...
[Cache] LRU HIT: ddaa617fe5992c9b...

Custom Handler Features (cache-handler.js)

  • Singleton Redis client pattern
  • Connection status tracking (isRedisConnected())
  • Retry strategy with exponential backoff
  • Graceful degradation to in-memory Map
  • TTL (Time To Live) support via revalidate config
  • Tag-based cache invalidation
  • Detailed logging for debugging

Key Methods:

async get(key, options)     // Retrieve cached data
async set(key, data, ctx)   // Store data with TTL
async revalidateTag(tag)    // Invalidate by tag

Multi-Pod Behavior

Pod Hostname Display

Every page shows the current pod's hostname at the top in a purple/blue banner:

Pod Hostname: nextjs-app-7d8f9c4b6-x5k2m

This helps you:

  • Verify load balancing is working
  • Debug pod-specific issues
  • Monitor cache behavior per pod
  • Understand traffic distribution

Cache Behavior Across Pods

With Redis (Shared Cache):

  • Pod A caches a page β†’ stored in Redis
  • Pod B receives next request β†’ cache HIT from Redis
  • All pods share the same cache
  • Optimal performance and consistency

Without Redis (LRU per-pod):

  • Pod A caches a page β†’ stored in Pod A's memory
  • Pod B receives next request β†’ cache MISS, generates new
  • Each pod has its own cache
  • Still works, but less efficient

Monitoring Cache Performance

Cache Logs

The Neshca handler includes detailed logging for every cache operation:

# Startup logs show which handlers are active
Cache handlers configured: [0] Redis Handler, [1] LRU Handler (in-memory)

# Cache operations (Redis)
[Cache] Redis SET: /app-isr/page
[Cache] Redis HIT: /app-isr/page
[Cache] Redis MISS: /app-ssg/detail/123

# Cache operations (LRU fallback)
[Cache] LRU SET: /app-isr/page
[Cache] LRU HIT: /app-isr/page
[Cache] LRU MISS: /new-page

# Connection events
[Redis] Connected successfully
[Redis] Ready to accept commands
[Redis] Error: Connection timeout

Kubernetes Monitoring

# View logs from all pods
kubectl logs -l app=nextjs-app -f

# View logs from specific pod
kubectl logs nextjs-app-xxx -f

# Check cache handler logs specifically
kubectl logs -l app=nextjs-app | grep "\[Cache\]"

# Monitor pod distribution
kubectl get pods -l app=nextjs-app -o wide

Data Structure

The application fetches photo data from JSONPlaceholder API with the following structure:

interface Item {
  id: number;          // Photo ID
  albumId: number;     // Album ID
  title: string;       // Photo title
  url: string;         // Full-size image URL (600x600)
  thumbnailUrl: string; // Thumbnail URL (150x150)
}

API Functions

// Fetch multiple items (returns first 20 photos)
const data = await getItems(revalidate?: number);
// Returns: ItemsResponse = { data: Item[] } | { error: string }

// Fetch single item by ID
const result = await getItemById(id: string, revalidate?: number);
// Returns: ItemResponse = { item: Item } | { error: string }

Environment Variables Reference

Variable Required Description Example
REDIS_URL No* Redis connection string redis://localhost:6379
KV_URL No* Alternative Redis connection string redis://redis-service:6379
REVALIDATION_SECRET No** Secret token for Pages Router revalidation API your-secret-token-here
PORT No Server port (default: 3000, Dockerfile: 4000) 4000

* Either REDIS_URL or KV_URL required for Redis caching. Falls back to LRU in-memory if neither is set.

** Required if using /api/revalidate-pages endpoint (Pages Router on-demand revalidation).

Note: The application uses the public JSONPlaceholder API which requires no authentication.

Testing & Quality Assurance

This project includes comprehensive testing tools and documentation:

πŸ“‹ Test Documentation

TESTING.md - Complete testing guide with:

  • Test cases organized by router type (App Router vs Pages Router)
  • Expected behaviors and failure signs
  • Manual testing procedures
  • Multi-pod testing scenarios
  • Success criteria checklist

See TESTING.md for the full testing guide.

PAGES-ROUTER-REVALIDATION.md - Pages Router specific guide:

  • How to revalidate Pages Router cache
  • Differences between App Router and Pages Router revalidation
  • Using /api/revalidate vs /api/revalidate-pages
  • Tag-based vs Path-based revalidation
  • Migration guide from Pages Router to App Router

See PAGES-ROUTER-REVALIDATION.md for the Pages Router revalidation guide.

πŸ”§ Interactive Testing Tools

1. Admin Panel (/admin)

  • Web UI for cache revalidation
  • Purge by path (e.g., /app-isr)
  • Purge by tags (e.g., photos, gallery-photos)
  • Quick action buttons for common operations
  • Live feedback on revalidation success

2. Cache Stats Dashboard (/stats)

  • Real-time Redis connection status
  • Cache key count and breakdown
  • Memory usage monitoring
  • Key details with TTL information
  • Auto-refresh option (every 5 seconds)

πŸ€– Automated Testing

test-cache.sh - Bash script for automated testing:

# Make executable
chmod +x test-cache.sh

# Run tests locally
./test-cache.sh http://localhost:3000

# Run tests against K8s deployment
./test-cache.sh https://your-loadbalancer.com

What it tests:

  • βœ… API Data Cache (/api/cached-fetch)
  • βœ… Force-dynamic behavior (/api/real-time)
  • βœ… Page ISR caching (/app-isr)
  • βœ… Tag-based revalidation
  • βœ… Image gallery loading
  • βœ… Cache statistics API
  • βœ… Multi-pod distribution (if in K8s)

Example output:

═══════════════════════════════════════════════════════════
  Test 1: API Route with fetch() - Data Cache
═══════════════════════════════════════════════════════════

ℹ️  INFO: Calling /api/cached-fetch for the first time...
βœ… PASS: Data Cache working - Second request was cached

═══════════════════════════════════════════════════════════
  Test Summary
═══════════════════════════════════════════════════════════

Passed: 7
Failed: 0

πŸŽ‰ All tests passed!

πŸ” API Testing Endpoints

Test cache behavior directly via API:

# Test cached fetch (Data Cache)
curl http://localhost:3000/api/cached-fetch | jq

# Test real-time API (No Cache)
curl http://localhost:3000/api/real-time | jq

# Purge by tag
curl "http://localhost:3000/api/revalidate?tags=photos" | jq

# Purge by path
curl "http://localhost:3000/api/revalidate?path=/app-isr" | jq

# Get cache stats
curl http://localhost:3000/api/cache-stats | jq

πŸ“Š Monitoring in Production

Redis CLI:

# View all Next.js cache keys
redis-cli KEYS "nextjs:*"

# Check specific key TTL
redis-cli TTL "nextjs:/app-isr/page"

# View revalidation tags
redis-cli HGETALL "nextjs:__revalidated_tags__"

# Monitor Redis memory
redis-cli INFO memory | grep used_memory_human

Kubernetes Logs:

# View cache logs from all pods
kubectl logs -l app=nextjs-app -f | grep "\[Cache\]"

# Check which pods are serving requests
kubectl logs -l app=nextjs-app --tail=100 | grep "Pod Hostname"

# Monitor cache handler initialization
kubectl logs -l app=nextjs-app --tail=100 | grep "Cache handlers configured"

Troubleshooting

App Router Pages Hanging (ISR/SSG)

Problem: App Router pages hang during development, Pages Router works fine

Solution: This was caused by Redis connection timeout. Now fixed with 5-second timeout in cache handler.

# Before fix:
Connecting Redis client...
(hangs indefinitely)

# After fix:
Connecting Redis client...
Failed to connect Redis client: Redis connection timeout
Falling back to LRU handler because Redis client is not available.
βœ“ App loads successfully

Redis Connection Issues

Problem: Failed to connect Redis client: Redis connection timeout

Solutions:

  1. Verify Redis is running: docker ps | grep redis
  2. Check Redis connection: redis-cli ping (should return PONG)
  3. Verify REDIS_URL in .env.local or environment variables
  4. For Kubernetes: Check service name and namespace

Cache Not Working

Problem: Data always fresh, never cached

Checklist:

  1. Verify cache handler is configured in next.config.ts
  2. Check logs for cache handler initialization
  3. Look for [Cache] SET and [Cache] HIT messages in logs
  4. Ensure revalidate is set correctly in fetch options
  5. Run in production mode (yarn build && yarn start)

Pod Hostname Not Showing

Problem: Hostname not displayed at top of pages

Solution: The hostname is fetched server-side in the root layout. Ensure:

  1. utils/hostname.ts exists
  2. components/PodHostname.tsx is imported in app/layout.tsx
  3. You're running the latest build

Build Errors

Problem: Redis connection errors during next build

Solution: The Neshca handler automatically skips Redis during build (NEXT_PHASE === PHASE_PRODUCTION_BUILD). If using custom handler, ensure proper phase detection.

Technologies Used

Performance Characteristics

With Redis (Multi-Pod Setup)

Metric Value
Cache Hit Time ~2-5ms
Cache Miss (First Request) ~100-500ms (API call)
Pod-to-Pod Consistency 100% (shared cache)
Revalidation Behavior Stale-while-revalidate

With LRU Fallback (Per-Pod)

Metric Value
Cache Hit Time <1ms (in-memory)
Cache Miss Rate Higher (separate caches)
Pod-to-Pod Consistency Eventually consistent
Memory Usage Configurable (default: 1000 items Γ— 100MB max)

Learn More

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Changelog

v2.0.0 (2026-01-09)

  • Added pod hostname display for multi-pod deployments
  • Implemented 5-second Redis connection timeout
  • Added comprehensive cache operation logging
  • Configured LRU memory limits
  • Fixed null handler filtering to prevent crashes
  • Added Kubernetes deployment manifests for EKS
  • Improved error handling and graceful degradation

v1.0.0

  • Initial release with Redis cache handler
  • App Router and Pages Router examples
  • Multiple caching strategies (SSG, ISR, SSR, Dynamic)

About

Centralize NextJS caching for all Pods

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •