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.
- 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
- 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
-
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)
-
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]
-
SSG (Static Site Generation)
- Pre-rendered at build time using
generateStaticParams - Maximum performance
- Ideal for rarely changing content
- Examples:
/app-ssg,/app-ssg/[id]
- Pre-rendered at build time using
-
SSR (Server-Side Rendering)
- Rendered on each request with
getServerSideProps - Always fresh data, no static caching
- Examples:
/page-server,/page-server/[id]
- Rendered on each request with
- Built from scratch using
ioredis - Direct control over caching logic
- Implements
get,set, andrevalidateTagmethods - Automatic fallback to Map-based in-memory cache
- Connection status monitoring
- Wait-until-ready pattern for initialization
- 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
- Node.js 18+
- Docker (for local Redis testing)
- Yarn or npm
- (Optional) AWS CLI and kubectl for EKS deployment
- Clone the repository:
git clone <repository-url>
cd nextjs-custom-cache- Install dependencies:
yarn install
# or
npm install- (Optional) Start Redis using Docker Compose:
docker-compose up -d redisOr 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- (Optional) Create
.env.localfile in the project root:
# Redis Configuration for Centralized Caching
# Format: redis://[:password@]host[:port][/db-number]
REDIS_URL=redis://localhost:6379yarn dev
# or
npm run devVisit http://localhost:3000 to see the application. The pod hostname will be displayed at the top of every page.
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 withgenerateStaticParams/gallery- Image gallery testing Next.js Image optimization and Buffer handling
Pages Router (Next.js 12):
/page-server- SSR list page withgetServerSideProps/page-server/1- SSR detail page/page-static- ISR list page withgetStaticProps/page-static/1- ISR detail page withgetStaticPaths
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
# Build the application
yarn build
# Start the production server
yarn startComplete instructions for deploying to Amazon EKS with multiple pods are available in k8s/README.md.
- 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-
Update image URL in
k8s/deployment.yaml -
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- 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!
# Scale to 5 pods
kubectl scale deployment nextjs-app --replicas=5
# Verify
kubectl get pods -l app=nextjs-app.
βββ 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)
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,
};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 |
The cache handlers support multiple environment variable names:
REDIS_URL- Primary Redis connection stringKV_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/0Enhanced 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...- 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
revalidateconfig - 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 tagEvery 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
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
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# 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 wideThe 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)
}// 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 }| 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.
This project includes comprehensive testing tools and 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/revalidatevs/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.
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)
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.comWhat 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!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 | jqRedis 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_humanKubernetes 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"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 successfullyProblem: Failed to connect Redis client: Redis connection timeout
Solutions:
- Verify Redis is running:
docker ps | grep redis - Check Redis connection:
redis-cli ping(should returnPONG) - Verify
REDIS_URLin.env.localor environment variables - For Kubernetes: Check service name and namespace
Problem: Data always fresh, never cached
Checklist:
- Verify cache handler is configured in
next.config.ts - Check logs for cache handler initialization
- Look for
[Cache] SETand[Cache] HITmessages in logs - Ensure
revalidateis set correctly in fetch options - Run in production mode (
yarn build && yarn start)
Problem: Hostname not displayed at top of pages
Solution: The hostname is fetched server-side in the root layout. Ensure:
utils/hostname.tsexistscomponents/PodHostname.tsxis imported inapp/layout.tsx- You're running the latest build
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.
- Next.js 16.1+ - React framework with App Router and Pages Router
- Redis - In-memory data store for distributed caching
- ioredis - Redis client for Node.js
- @neshca/cache-handler - Production-ready cache handler
- JSONPlaceholder - Free REST API for testing (5000 photos dataset)
- TailwindCSS 4 - Modern utility-first CSS
- TypeScript - Full type safety
- Docker - Container platform for Redis and app
- Kubernetes - Container orchestration for multi-pod deployment
- Amazon EKS - Managed Kubernetes service
| 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 |
| 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) |
- Next.js Caching Documentation
- Custom Cache Handler Guide
- Redis Caching Patterns
- Neshca Cache Handler Docs
- Kubernetes Documentation
- Amazon EKS Best Practices
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- 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
- Initial release with Redis cache handler
- App Router and Pages Router examples
- Multiple caching strategies (SSG, ISR, SSR, Dynamic)