AWS ElastiCache Redis: Caching Strategies, Session Storage, Rate Limiting, and Pub/Sub
Use AWS ElastiCache Redis for production caching, session storage, rate limiting, and pub/sub in Node.js applications. Covers cache-aside pattern, write-through, Redis Cluster vs Replication Group, Terraform setup, and ioredis TypeScript patterns.
ElastiCache Redis is the managed Redis option on AWS — handles replication, failover, backups, and patching. The question isn't whether to use it, but how: cache-aside vs write-through, single node vs cluster, how to structure cache keys, and what to do when the cache is cold.
This guide covers the production patterns for ElastiCache with TypeScript and ioredis.
Terraform: ElastiCache Replication Group
# terraform/elasticache.tf
# Subnet group for Redis nodes
resource "aws_elasticache_subnet_group" "redis" {
name = "${var.app_name}-redis"
subnet_ids = var.private_subnet_ids # Redis must be in private subnets
tags = local.tags
}
# Parameter group for Redis 7 tuning
resource "aws_elasticache_parameter_group" "redis" {
name = "${var.app_name}-redis7"
family = "redis7"
parameter {
name = "maxmemory-policy"
value = "allkeys-lru" # Evict least-recently-used keys when memory full
}
parameter {
name = "activerehashing"
value = "yes" # Background rehashing for consistent latency
}
parameter {
name = "lazyfree-lazy-eviction"
value = "yes" # Async eviction (non-blocking)
}
}
# Replication group: 1 primary + 1 replica (Multi-AZ)
resource "aws_elasticache_replication_group" "redis" {
replication_group_id = "${var.app_name}-redis"
description = "${var.app_name} Redis cache"
node_type = "cache.t4g.medium" # Graviton2: 20% cheaper
num_cache_clusters = 2 # 1 primary + 1 replica
parameter_group_name = aws_elasticache_parameter_group.redis.name
subnet_group_name = aws_elasticache_subnet_group.redis.name
security_group_ids = [aws_security_group.redis.id]
# Redis 7.x
engine_version = "7.1"
port = 6379
# Encryption at rest and in transit
at_rest_encryption_enabled = true
transit_encryption_enabled = true
auth_token = var.redis_auth_token # Stored in AWS Secrets Manager
# Automatic failover: promote replica if primary fails (~30s)
automatic_failover_enabled = true
multi_az_enabled = true
# Daily backups at 2am UTC, retained 7 days
snapshot_retention_limit = 7
snapshot_window = "02:00-03:00"
maintenance_window = "sun:04:00-sun:05:00"
tags = local.tags
}
# Security group: only allow access from app security group
resource "aws_security_group" "redis" {
name = "${var.app_name}-redis"
vpc_id = var.vpc_id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [var.app_security_group_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.tags
}
# Output: primary endpoint for app configuration
output "redis_endpoint" {
value = aws_elasticache_replication_group.redis.primary_endpoint_address
sensitive = false
}
Redis Client Setup with ioredis
// lib/redis.ts
import Redis from "ioredis";
let redis: Redis | null = null;
export function getRedis(): Redis {
if (!redis) {
redis = new Redis({
host: process.env.REDIS_HOST!, // ElastiCache primary endpoint
port: parseInt(process.env.REDIS_PORT ?? "6379"),
password: process.env.REDIS_AUTH_TOKEN, // TLS auth token
tls: process.env.NODE_ENV === "production" ? {} : undefined,
// Connection pooling and retry
maxRetriesPerRequest: 3,
retryStrategy(times: number) {
if (times > 5) return null; // Stop retrying after 5 attempts
return Math.min(times * 100, 2000); // Exponential backoff: 100ms, 200ms, 400ms...
},
enableOfflineQueue: true, // Queue commands while reconnecting
// Key namespace (prevents collisions if sharing a Redis instance)
keyPrefix: `${process.env.REDIS_PREFIX ?? "app"}:`,
});
redis.on("error", (err) => console.error("[redis] Error:", err));
redis.on("connect", () => console.log("[redis] Connected"));
redis.on("reconnecting",() => console.log("[redis] Reconnecting..."));
}
return redis;
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Pattern 1: Cache-Aside
Read from cache; on miss, read from DB and populate cache.
// lib/cache/workspace.ts
import { getRedis } from "@/lib/redis";
import { prisma } from "@/lib/prisma";
const WORKSPACE_TTL = 300; // 5 minutes
interface WorkspaceData {
id: string;
name: string;
plan: string;
slug: string;
}
export async function getWorkspace(id: string): Promise<WorkspaceData | null> {
const redis = getRedis();
const key = `workspace:${id}`;
// 1. Try cache
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached) as WorkspaceData;
}
// 2. Cache miss: read from DB
const workspace = await prisma.workspace.findUnique({
where: { id },
select: { id: true, name: true, plan: true, slug: true },
});
if (!workspace) return null;
// 3. Populate cache with TTL
await redis.setex(key, WORKSPACE_TTL, JSON.stringify(workspace));
return workspace;
}
// Invalidate cache when workspace is updated
export async function invalidateWorkspace(id: string): Promise<void> {
const redis = getRedis();
await redis.del(`workspace:${id}`);
}
Pattern 2: Cache Stampede Prevention with Lock
When the cache is cold and many requests arrive simultaneously, they all hit the DB at once. Use a Redis lock to let only one request regenerate:
// lib/cache/with-lock.ts
import { getRedis } from "@/lib/redis";
export async function cachedWithLock<T>(
key: string,
ttl: number,
fetchFn: () => Promise<T>,
lockTtl = 10 // seconds to hold the lock
): Promise<T> {
const redis = getRedis();
const lockKey = `lock:${key}`;
// 1. Try cache first
const cached = await redis.get(key);
if (cached) return JSON.parse(cached) as T;
// 2. Try to acquire lock (SET NX = only if not exists)
const acquired = await redis.set(lockKey, "1", "EX", lockTtl, "NX");
if (acquired === "OK") {
try {
// We hold the lock — fetch from DB
const data = await fetchFn();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
} finally {
await redis.del(lockKey);
}
} else {
// Another request is fetching — poll until cache is populated
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 100));
const result = await redis.get(key);
if (result) return JSON.parse(result) as T;
}
// Fallback: fetch directly
return fetchFn();
}
}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Pattern 3: Rate Limiting with Lua
// lib/rate-limit/redis-limiter.ts
import { getRedis } from "@/lib/redis";
const RATE_LIMIT_SCRIPT = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
local ttl = redis.call('TTL', key)
return {0, current, ttl} -- {allowed=0, count, retry_after}
end
return {1, current, window} -- {allowed=1, count, window}
`;
export interface RateLimitResult {
allowed: boolean;
count: number;
retryAfter: number;
}
export async function checkRateLimit(
identifier: string, // e.g. `ip:${ip}` or `user:${userId}`
limit: number, // Max requests
windowSec: number // Time window in seconds
): Promise<RateLimitResult> {
const redis = getRedis();
const key = `ratelimit:${identifier}`;
const result = await redis.eval(
RATE_LIMIT_SCRIPT,
1,
key,
limit.toString(),
windowSec.toString()
) as [number, number, number];
return {
allowed: result[0] === 1,
count: result[1],
retryAfter: result[0] === 0 ? result[2] : 0,
};
}
// Usage in Next.js route handler
export async function withRateLimit(
req: Request,
identifier: string,
limit = 60,
windowSec = 60
): Promise<Response | null> {
const result = await checkRateLimit(identifier, limit, windowSec);
if (!result.allowed) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"Retry-After": result.retryAfter.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": (Date.now() / 1000 + result.retryAfter).toString(),
},
});
}
return null; // Allowed
}
Pattern 4: Pub/Sub for Real-Time Events
// lib/pubsub/redis-pubsub.ts
import Redis from "ioredis";
// Pub/Sub requires a separate connection (subscriber cannot send regular commands)
let publisher: Redis | null = null;
let subscriber: Redis | null = null;
function createRedisConnection(): Redis {
return new Redis({
host: process.env.REDIS_HOST!,
port: parseInt(process.env.REDIS_PORT ?? "6379"),
password: process.env.REDIS_AUTH_TOKEN,
tls: process.env.NODE_ENV === "production" ? {} : undefined,
});
}
export function getPublisher(): Redis {
if (!publisher) publisher = createRedisConnection();
return publisher;
}
export function getSubscriber(): Redis {
if (!subscriber) subscriber = createRedisConnection();
return subscriber;
}
// Publish an event to a channel
export async function publish(channel: string, data: unknown): Promise<void> {
await getPublisher().publish(channel, JSON.stringify(data));
}
// Subscribe to a channel (long-lived; set up once per process)
export function subscribe(
channel: string,
handler: (data: unknown) => void
): () => void {
const sub = getSubscriber();
sub.subscribe(channel, (err) => {
if (err) console.error(`[pubsub] Subscribe error on ${channel}:`, err);
});
const messageHandler = (ch: string, message: string) => {
if (ch === channel) {
try {
handler(JSON.parse(message));
} catch (err) {
console.error("[pubsub] Message parse error:", err);
}
}
};
sub.on("message", messageHandler);
// Return unsubscribe function
return () => {
sub.unsubscribe(channel);
sub.off("message", messageHandler);
};
}
// Usage: broadcast workspace events to all connected users
// publish(`workspace:${workspaceId}:events`, { type: "member.added", userId });
// subscribe(`workspace:${workspaceId}:events`, handleEvent);
Cost Estimates (2026 Pricing)
| Node Type | vCPU | RAM | Price/Month |
|---|---|---|---|
| cache.t4g.micro | 2 | 0.5 GB | ~$12 |
| cache.t4g.small | 2 | 1.4 GB | ~$24 |
| cache.t4g.medium | 2 | 3.1 GB | ~$48 |
| cache.r7g.large | 2 | 13 GB | ~$130 |
| cache.r7g.xlarge | 4 | 26 GB | ~$260 |
Add ~50% for Multi-AZ replica. Savings Plans available for 1-year commitment (20% discount).
See Also
- Redis Advanced Patterns
- SaaS Feature Flags Advanced
- Next.js Rate Limiting
- AWS Lambda VPC Configuration
- AWS SQS SNS Patterns
Working With Viprasol
ElastiCache configuration details compound — wrong eviction policy, missing TLS auth, no reconnect strategy — and they're invisible until traffic spikes. Our team provisions ElastiCache with Graviton2 nodes, allkeys-lru eviction, TLS + auth token from Secrets Manager, and Multi-AZ failover. Application patterns include cache-aside with stampede prevention (SET NX lock), atomic Lua rate limiting, and separate pub/sub connections.
What we deliver:
- Terraform: replication group, subnet group, parameter group (allkeys-lru, lazyfree), security group, daily backups
getRedis()singleton withkeyPrefix, retry strategy, TLS, and offline queuecachedWithLock: SET NX lock to prevent cache stampede, poll-then-fallbackcheckRateLimit: atomic Lua INCR+EXPIRE script, returns allowed/count/retryAfterwithRateLimitmiddleware withRetry-After/X-RateLimit-*headerspublish/subscribehelpers with dedicated pub/sub connection
Talk to our team about your Redis caching architecture →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.