Amazon ElastiCache: A Complete Developer Guide
Amazon ElastiCache is a fully managed, in-memory caching service that supports two popular open-source engines: Redis and Memcached. It provides sub-millisecond latency at massive scale by keeping data in memory rather than on disk, making it a critical component for modern, high-performance applications. This tutorial covers everything you need to know about running ElastiCache effectively—from provisioning your first cluster to mastering cost, security, and performance optimization in production.
What Is ElastiCache?
ElastiCache acts as a high-speed data layer between your application and your primary database. Instead of querying a disk-based database for every request—which can take tens or hundreds of milliseconds—your application first checks the cache. If the data exists (a cache hit), it's returned almost instantly. If not (a cache miss), the application fetches from the database and writes the result back to the cache for subsequent requests. This pattern dramatically reduces database load and improves response times.
ElastiCache handles cluster provisioning, hardware failure detection, node replacement, software patching, and backups—all tasks that would otherwise consume significant engineering time if you self-managed Redis or Memcached on EC2 instances.
Why ElastiCache Matters
For developers building web applications, APIs, real-time analytics, gaming leaderboards, or session stores, ElastiCache delivers tangible benefits that compound as your user base grows:
- Cost savings on primary databases: By offloading 70–90% of read queries to the cache, you can often downsize your RDS or DynamoDB instances significantly.
- Predictable latency at scale: A database under heavy load may spike to 200ms+ response times; a cache consistently returns data in under 1ms.
- Resilience during traffic surges: Caches absorb read-heavy spikes (e.g., product launches, flash sales) without overwhelming the primary database.
- Built-in high availability: With replication groups and automatic failover, you get production-grade reliability without operational overhead.
- Real-time use cases unlocked: Pub/sub messaging, leaderboard scoring with sorted sets, and session management all become trivial with Redis data structures.
Understanding the Two Engines: Redis vs. Memcached
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before diving into best practices, you must choose the right engine for your workload. The decision impacts available features, cost structure, and operational patterns.
Redis (Cluster Mode Disabled / Enabled)
Redis on ElastiCache offers rich data structures (strings, hashes, lists, sets, sorted sets, streams), persistence via snapshots and append-only files, replication with automatic failover, and advanced features like Lua scripting and pub/sub. Redis supports two cluster modes:
- Cluster Mode Disabled: A single shard with a primary node and up to 5 read replicas. Best for workloads under ~25GB total dataset size where you need simple failover.
- Cluster Mode Enabled: Horizontally scalable across multiple shards (up to 500). Each shard has a primary and replicas. Data is partitioned across shards using hash slots (16,384 slots total). Ideal for datasets larger than 25GB or write-heavy workloads requiring sharding.
Memcached
Memcached is a simpler, multi-threaded cache without persistence, replication, or advanced data structures. It scales horizontally by adding nodes to a cluster, with data partitioned across nodes via client-side hashing. Memcached excels at simple key-value caching where you need the absolute highest throughput with minimal feature overhead.
Quick selection guide:
- Need persistence, replication, failover, or complex data types? Choose Redis.
- Need simple, blazing-fast key-value caching with multi-threaded performance? Choose Memcached.
- Need to scale writes beyond a single node? Choose Redis Cluster Mode Enabled.
Cost Optimization Best Practices
ElastiCache pricing is based on node hours, data transfer, and optional features. Without careful planning, costs can spiral—especially if you over-provision or neglect reserved instance pricing.
1. Right-Size Your Node Types
ElastiCache offers a wide range of instance families. The key is matching your workload profile to the right family:
- General-purpose (M-family, e.g., cache.m7g.large): Balanced compute/memory. Start here for most workloads.
- Memory-optimized (R-family, e.g., cache.r7g.xlarge): High memory-to-CPU ratio. Ideal for large datasets with moderate throughput.
- Compute-optimized (C-family, e.g., cache.c7gn.large): High CPU for compute-heavy operations like Lua scripts, sorting, or encryption.
- Graviton-based (m7g, r7g, c7g): ARM-based instances offering 20–30% cost savings over equivalent x86 instances with comparable performance.
Practical monitoring approach: Use CloudWatch metrics to determine if you're over-provisioned.
# AWS CLI: Check CPU utilization for the last 7 days
aws cloudwatch get-metric-statistics \
--namespace AWS/ElastiCache \
--metric-name CPUUtilization \
--dimensions Name=CacheClusterId,Value=my-redis-cluster \
--start-time $(date -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Average
# If CPU is consistently below 20%, consider downsizing or
# consolidating workloads onto fewer nodes.
2. Purchase Reserved Nodes
Reserved nodes offer significant discounts (30–60%) over on-demand pricing for predictable, steady-state workloads. You commit to a 1-year or 3-year term.
# Terraform example: Reserved node equivalent via on-demand
# (Reserved nodes are purchased via AWS Console/CLI, not provisioned)
# This shows a typical cluster that benefits from reserved pricing
resource "aws_elasticache_replication_group" "production" {
replication_group_id = "prod-redis"
description = "Production Redis with reserved node coverage"
# Purchase reserved nodes separately via:
# aws elasticache purchase-reserved-cache-nodes-offering
# --reserved-cache-nodes-offering-id "your-offering-id"
node_type = "cache.r7g.xlarge"
engine = "redis"
cache_node_type = "cache.r7g.xlarge"
# 1-year all-upfront reserved for this node type saves ~30%
# 3-year all-upfront saves ~50%
}
3. Avoid Unnecessary Read Replicas
Each read replica doubles your compute cost (or more, with multiple replicas). Evaluate whether you truly need them:
- You need at least one replica for Redis failover (multi-AZ with automatic failover). This is non-negotiable for production.
- Additional replicas only help if your read volume exceeds what the primary + one replica can handle.
- For Memcached, there are no replicas—you simply add more nodes for capacity, which also increases cost linearly.
# Check cache hit rate to determine if you need more cache capacity
# Low hit rate + high evictions = undersized cache (need more memory/nodes)
aws cloudwatch get-metric-statistics \
--namespace AWS/ElastiCache \
--metric-name CacheHitRate \
--dimensions Name=CacheClusterId,Value=my-redis-cluster \
--start-time $(date -d '1 day ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Average
# Target: CacheHitRate > 90% for read-heavy workloads
# If hit rate is already 95%+, adding replicas won't help—
# you're already serving most reads from cache.
4. Use Appropriate Cache Strategies to Reduce Required Capacity
The caching pattern you implement directly impacts how much memory you need:
- Lazy loading (cache-aside): Only cache actively requested data. Misses cause database reads, but cache doesn't fill with unused data. Most memory-efficient.
- Write-through: Every write goes to both cache and database. Cache always has fresh data but may store items never read.
- TTL tuning: Set appropriate expiration times. Shorter TTLs reduce memory pressure but increase miss rate.
# Python example: Lazy loading with configurable TTL
import redis
import json
import time
cache = redis.Redis(host='my-redis-cluster.xx-cluster.aps1.cache.amazonaws.com',
port=6379, decode_responses=True)
def get_user_profile(user_id: str) -> dict:
cache_key = f"user:profile:{user_id}"
# Try cache first
cached = cache.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss: fetch from database
profile = db.fetch_user_profile(user_id) # Your DB call
# Store in cache with a cost-aware TTL
# Frequent-access data: 1 hour TTL
# Infrequent-access data: 5 minute TTL to save memory
ttl = 3600 if profile.get('premium_user') else 300
cache.setex(cache_key, ttl, json.dumps(profile))
return profile
5. Monitor and Eliminate Idle Clusters
Development, staging, and one-off testing clusters often run 24/7 unnecessarily. Implement automated shutdown scripts.
# Bash script: Stop non-production clusters during off-hours
# Run via cron or CI/CD pipeline
NON_PROD_CLUSTERS=("dev-redis" "staging-redis" "qa-redis")
for cluster in "${NON_PROD_CLUSTERS[@]}"; do
# Check if cluster exists and is available
status=$(aws elasticache describe-cache-clusters \
--cache-cluster-id "$cluster" \
--query "CacheClusters[0].CacheClusterStatus" \
--output text 2>/dev/null)
if [ "$status" = "available" ]; then
echo "Stopping cluster: $cluster"
aws elasticache delete-cache-cluster --cache-cluster-id "$cluster"
# Note: Deleting is destructive. For Redis with persistence,
# take a snapshot first:
# aws elasticache create-snapshot --replication-group-id "$cluster" \
# --snapshot-name "${cluster}-pre-shutdown-$(date +%Y%m%d)"
fi
done
Security Best Practices
ElastiCache stores sensitive data in memory—session tokens, user profiles, API rate limit counters, and sometimes even PII. A security breach in your cache layer can be catastrophic.
1. Network Isolation: Always Use VPC
Never deploy ElastiCache in EC2-Classic mode (legacy). Always place clusters inside a VPC with private subnets. The cache should never be directly accessible from the public internet.
# Terraform: VPC-private ElastiCache subnet group
resource "aws_elasticache_subnet_group" "private" {
name = "elasticache-private-subnet"
subnet_ids = [
aws_subnet.private_az1.id,
aws_subnet.private_az2.id,
aws_subnet.private_az3.id,
]
tags = {
Name = "elasticache-private-subnet-group"
Environment = "production"
Access = "private-only"
}
}
resource "aws_elasticache_replication_group" "secure" {
replication_group_id = "secure-redis"
subnet_group_name = aws_elasticache_subnet_group.private.name
# No public access—this is the default and should stay this way
# The cluster is only reachable from within the VPC
}
2. Security Groups: Principle of Least Privilege
Create dedicated security groups that only allow inbound traffic from specific application security groups on the Redis/Memcached port. Never open port 6379 or 11211 to 0.0.0.0/0.
# Terraform: Strict security group for ElastiCache
resource "aws_security_group" "elasticache" {
name = "elasticache-redis-sg"
description = "Allow Redis traffic only from app tier"
vpc_id = aws_vpc.main.id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
# Only allow the application security group
security_groups = [aws_security_group.app_tier.id]
description = "Redis from application servers only"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
# Outbound is fine for VPC DNS, CloudWatch, etc.
}
tags = {
Name = "elasticache-sg"
}
}
resource "aws_elasticache_replication_group" "secure" {
security_group_ids = [aws_security_group.elasticache.id]
# ... other configuration
}
3. Encryption in Transit and at Rest
Enable both encryption layers, especially for production workloads handling user data:
- Encryption at rest: Uses AWS KMS to encrypt data on disk (for Redis persistence/snapshots). Enabled at cluster creation and cannot be changed later.
- Encryption in transit: TLS between clients and the cache, and between primary and replica nodes. Available for Redis 6.0+ and all Memcached versions.
# Terraform: Fully encrypted Redis cluster
resource "aws_elasticache_replication_group" "encrypted" {
replication_group_id = "encrypted-redis"
# Encryption at rest
at_rest_encryption_enabled = true
# Optional: specify a customer-managed KMS key
# kms_key_id = aws_kms_key.elasticache.arn
# Encryption in transit
transit_encryption_enabled = true
# With transit encryption, you must provide an auth token
auth_token = var.redis_auth_token # Store in secrets manager!
# Use a recent Redis version with TLS support
engine_version = "7.1"
}
# Client connection example with TLS (Python)
import redis
import ssl
redis_client = redis.Redis(
host='encrypted-redis.xx-cluster.aps1.cache.amazonaws.com',
port=6379,
password='your-auth-token',
ssl=True,
ssl_cert_reqs=ssl.CERT_REQUIRED,
ssl_ca_certs='/etc/ssl/certs/ca-certificates.crt',
decode_responses=True
)
4. Redis AUTH Token Management
Redis authentication tokens (Redis ACLs in 6.0+, or the older auth_token parameter) add a password layer. Treat these as secrets—store them in AWS Secrets Manager and rotate regularly.
# Store Redis AUTH token in Secrets Manager
aws secretsmanager create-secret \
--name "prod/elasticache/redis-auth-token" \
--description "Redis AUTH token for encrypted cluster" \
--secret-string '{"auth_token":"$(openssl rand -base64 32)"}'
# Application retrieves token at startup
import boto3
import redis
def get_redis_client():
secrets = boto3.client('secretsmanager')
secret = secrets.get_secret_value(
SecretId='prod/elasticache/redis-auth-token'
)
auth_token = json.loads(secret['SecretString'])['auth_token']
return redis.Redis(
host='your-cluster-endpoint',
port=6379,
password=auth_token,
ssl=True,
decode_responses=True
)
5. Redis ACLs for Multi-Tenant Access Control
With Redis 6.0+, you can create named users with fine-grained permissions. This is invaluable for microservices that share a cluster but should only access their own key patterns.
# Create Redis ACL users via the Redis CLI or configuration
# Example: Create users with key-pattern restrictions
# Connect to primary node
redis-cli -h primary-node.endpoint -p 6379 --tls --cacert ca.pem -a admin-password
# Create a user for the payment service
ACL SETUSER payment-service \
on \
>payment-service-password \
~payment:* ~transaction:* \
+@all \
-@dangerous
# Create a user for the session service (read-only on session keys)
ACL SETUSER session-service \
on \
>session-service-password \
~session:* \
+get +mget +hget +hgetall +hmget \
-@all
# Create a read-only analytics user
ACL SETUSER analytics-readonly \
on \
>analytics-password \
~* \
+get +mget +hget +hgetall +zrange +zrangebyscore \
-@all
# Save ACLs to persist across restarts
ACL SAVE
6. Audit Logging and Monitoring
Enable CloudWatch logs and metrics. For Redis, log slow queries to identify potential security issues or performance problems. Set up alerts for authentication failures.
# CloudFormation snippet: Enable CloudWatch logs for ElastiCache
# (Redis Engine Logs and Slow Queries)
{
"LogDeliveryConfigurations": [
{
"DestinationDetails": {
"CloudWatchLogsDestinationDetails": {
"LogGroup": "/aws/elasticache/prod-redis"
}
},
"LogFormat": "json",
"LogType": "engine-log",
"DestinationType": "cloudwatch-logs"
},
{
"DestinationDetails": {
"CloudWatchLogsDestinationDetails": {
"LogGroup": "/aws/elasticache/prod-redis-slow"
}
},
"LogFormat": "json",
"LogType": "slow-log",
"DestinationType": "cloudwatch-logs"
}
]
}
Performance Best Practices
Performance tuning for ElastiCache spans client configuration, data modeling, cluster topology, and operational monitoring. The goal is to maintain sub-millisecond latency while maximizing throughput.
1. Connection Management: Pool Connections Aggressively
Redis and Memcached are single-threaded per connection (Redis) or per-event-loop (Memcached). Opening hundreds of connections per application server wastes resources and increases latency. Use a connection pool and reuse connections.
# Python: Connection pooling with redis-py
import redis
from redis import ConnectionPool
# Create a global pool—share across your entire application
# Rule of thumb: pool size = (max DB connections) / (number of app instances)
connection_pool = ConnectionPool(
host='your-redis-endpoint',
port=6379,
password='your-password',
ssl=True,
max_connections=20, # Per application instance
socket_keepalive=True,
socket_connect_timeout=2, # Fail fast if network issues
socket_timeout=2, # Don't block indefinitely
retry_on_timeout=True,
health_check_interval=30 # Proactive health checks
)
# Use the pool everywhere
cache = redis.Redis(connection_pool=connection_pool)
# Node.js equivalent using ioredis
const Redis = require('ioredis');
const redis = new Redis.Cluster([
{ host: 'primary-endpoint', port: 6379 },
], {
redisOptions: {
password: 'your-password',
tls: {},
maxRetriesPerRequest: 3,
connectTimeout: 2000,
keepAlive: true,
},
maxRedirections: 16,
retryDelayOnFailover: 100,
scaleReads: 'master', // or 'slave' for read replicas
});
2. Client-Side Caching Patterns for Maximum Performance
For ultra-low latency (sub-100μs), implement local in-process caching in front of ElastiCache. This is called two-tier caching or cache-aside with local L1 cache.
# Python: Two-tier caching with LRU local cache + Redis
from functools import lru_cache
import redis
import hashlib
import json
cache = redis.Redis(connection_pool=global_pool)
# L1: In-process LRU cache (microsecond latency)
# L2: Redis ElastiCache (sub-millisecond latency)
# L3: Database (milliseconds)
@lru_cache(maxsize=1024)
def get_product(product_id: str) -> dict:
"""Two-tier cache: check L1 (memory) then L2 (Redis) then DB"""
cache_key = f"product:{product_id}"
# L2: Redis
cached = cache.get(cache_key)
if cached:
return json.loads(cached)
# L3: Database
product = db.fetch_product(product_id)
# Write to L2 with staggered TTL to prevent thundering herd
base_ttl = 3600 # 1 hour
jitter = random.randint(0, 300) # 0-5 minutes of jitter
cache.setex(cache_key, base_ttl + jitter, json.dumps(product))
return product
# The @lru_cache decorator keeps hot items in process memory
# This means 80%+ of reads never touch the network
3. Avoid Costly Redis Commands in Production
Certain Redis commands block the entire server for the duration of their execution. On a single-threaded engine, this means all other clients wait. Avoid these in production:
- KEYS * — Scans the entire keyspace. Use
SCANwith a cursor instead. - FLUSHALL / FLUSHDB — Deletes all keys. Blocking and dangerous.
- SORT on large datasets — Use sorted sets instead.
- LREM, SREM on massive collections — Consider splitting large collections.
# BAD: Blocks Redis for potentially seconds on large datasets
redis-cli> KEYS user:session:*
# GOOD: Incremental scanning with SCAN
# Python example: Safe key iteration
def scan_keys(pattern: str, batch_size: int = 100):
"""Safely iterate keys matching a pattern using SCAN"""
cursor = 0
keys = []
while True:
cursor, batch = cache.scan(
cursor=cursor,
match=pattern,
count=batch_size
)
keys.extend(batch)
if cursor == 0:
break
return keys
# Even better: Store related keys in a Redis Set for O(1) retrieval
# Instead of: KEYS user:session:*
# Maintain: SADD user_sessions:active {session_id}
# Retrieve: SMEMBERS user_sessions:active
4. Sharding Strategy for Redis Cluster Mode Enabled
When scaling writes beyond a single node, you need Cluster Mode Enabled. Data is partitioned across shards using 16,384 hash slots. The key is choosing a sharding key that distributes data evenly.
# Key design for even shard distribution
# Redis uses CRC16(key) mod 16384 to determine hash slot
# BAD: Sequential user IDs may cluster on one shard
# cache.set(f"user:profile:{sequential_id}", data) # Hot shard risk
# GOOD: Use a well-distributed hash or add a hash tag for related keys
# Use hash tags {} to force related keys onto the same shard
import hashlib
def get_shard_key(user_id: str) -> str:
"""Generate a consistent hash tag for related user keys"""
# All keys for user_id will land on the same shard
# This enables atomic operations on related keys
shard_id = hashlib.md5(user_id.encode()).hexdigest()[:4]
return f"user:{shard_id}:{user_id}"
# Usage: All related user keys share the hash tag
user_shard_key = get_shard_key("user_12345")
cache.set(f"user:profile:{{{user_shard_key}}}", profile_json)
cache.set(f"user:sessions:{{{user_shard_key}}}", sessions_json)
cache.hset(f"user:activity:{{{user_shard_key}}}", "last_login", timestamp)
# These three keys will always be on the same shard,
# enabling multi-key transactions and pipelining
5. Pipelining and Batching for High Throughput
Network round-trips dominate latency at scale. Pipelining sends multiple commands in a single network call, dramatically improving throughput for bulk operations.
# Python: Pipelining for bulk writes
def bulk_set_user_sessions(sessions: dict) -> None:
"""Write 100+ session keys in one network round-trip"""
pipeline = cache.pipeline()
for session_id, session_data in sessions.items():
key = f"session:{session_id}"
pipeline.setex(key, 1800, json.dumps(session_data))
# All commands sent in one batch, results collected together
pipeline.execute()
# For even higher throughput, use asynchronous pipelines
import asyncio
import aioredis
async def async_bulk_load(keys_and_values: dict):
"""Async pipeline for maximum throughput"""
redis = await aioredis.from_url(
"redis://your-endpoint",
password="your-password",
ssl=True
)
async with redis.pipeline() as pipe:
for key, value in keys_and_values.items():
pipe.set(key, value)
await pipe.execute()
6. Monitor and Alert on Key Performance Metrics
Set up CloudWatch alarms for metrics that indicate performance degradation before users notice:
- CacheHitRate: Below 90% warrants investigation.
- Evictions: Spiking evictions mean you're running out of memory—scale up or adjust TTLs.
- CPUUtilization: Sustained above 70% on Redis (single-threaded) indicates you're approaching throughput limits.
- CurrConnections: Sudden spikes may indicate connection leaks in your application.
- ReplicationLag: On replicas, lag > 10 seconds indicates the primary is outpacing replica ingestion.
# CloudWatch alarm for high evictions (memory pressure)
aws cloudwatch put-metric-alarm \
--alarm-name "prod-redis-high-evictions" \
--alarm-description "Alert when evictions exceed threshold" \
--namespace "AWS/ElastiCache" \
--metric-name "Evictions" \
--dimensions Name=CacheClusterId,Value=prod-redis-001 \
--statistic "Sum" \
--period 300 \
--evaluation-periods 2 \
--threshold 1000 \
--comparison-operator "GreaterThanThreshold" \
--alarm-actions "arn:aws:sns:us-east-1:123456789:ops-alerts"
# CloudWatch alarm for low cache hit rate
aws cloudwatch put-metric-alarm \
--alarm-name "prod-redis-low-hit-rate" \
--alarm-description "Cache hit rate below 85%" \
--namespace "AWS/ElastiCache" \
--metric-name "CacheHitRate" \
--dimensions Name=CacheClusterId,Value=prod-redis-001 \
--statistic "Average" \
--period 300 \
--evaluation-periods 3 \
--threshold 85 \
--comparison-operator "LessThanThreshold" \
--alarm-actions "arn:aws:sns:us-east-1:123456789:ops-alerts"
7. Optimize Serialization for Speed
The format you choose for cache values impacts both memory efficiency and serialization/deserialization speed:
- MessagePack: Faster and more compact than JSON. Excellent for high-throughput APIs.
- Protocol Buffers / FlatBuffers: Schema-based, extremely fast deserialization. Ideal for microservices with well-defined data contracts.
- Compressed JSON: Use for large payloads where you want to trade CPU for reduced memory.
# Python: Benchmark serialization options
import json
import msgpack
import time
import sys
test_data = {
"user_id": "abc123",
"profile": {"name": "John", "preferences": {"theme": "dark"}},
"scores": [100, 95, 87, 92, 88]
}
# JSON benchmark
start = time.perf_counter()
json_bytes = json.dumps(test_data).encode('utf-8')
json_deserialized = json.loads(json_bytes)
json_time = (time.perf_counter() - start) * 1000 # ms
# MessagePack benchmark
start = time.perf_counter()
msgpack_bytes = msgpack.packb(test_data)
msgpack_deserialized = msgpack.unpackb(msgpack_bytes)
msgpack_time = (time.perf_counter() - start) * 1000
print(f"JSON: {len(json_bytes)} bytes, {json_time:.3f}ms")
print(f"MessagePack: {len(msgpack_bytes)} bytes, {msgpack_time:.3f}ms")
# Typical result: MessagePack is 30-50% faster and 20-30% smaller
8. Graceful Degradation with Circuit Breakers
Even the best-managed ElastiCache cluster can experience brief hiccups—failovers, network blips, or maintenance events. Your application must handle these gracefully without cascading failures.
# Python: Circuit breaker pattern for cache operations
import redis
from functools import wraps
import time
class CacheCircuitBreaker:
def __init__(self, failure_threshold=5, recovery_time=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_time = recovery_time
self.last_failure_time = 0
self.state = "closed" # closed, open, half-open
def call(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_time:
self.state = "half-open"
else:
# Circuit is open: fast-fail, go directly to DB
return None
try:
result = func(*args, **kwargs)
if self.state == "half-open":
self.state = "closed"
self.failure_count = 0
return result
except (redis.ConnectionError, redis.TimeoutError) as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
# Graceful degradation: return None, caller falls back to DB
return None
return wrapper
breaker = CacheCircuitBreaker()
@breaker.call
def cached_get(key: str):
return cache.get(key)
# Usage: If cache is unavailable, this returns None immediately
# rather than blocking for seconds on a timeout
value = cached_get("user:profile:123")
if value is None:
# Fall back to database
value = db.fetch_user_profile("123")
Putting It All Together: A Production-Ready ElastiCache Setup
Here is a complete Terraform configuration that implements the cost, security, and performance best practices discussed above. It creates a production Redis cluster with encryption, proper networking, monitoring, and right-sized instances:
# Complete production ElastiCache setup
# File: elasticache_production.tf
# 1. Networking: Private subnets and dedicated security group
resource "aws_security_group" "redis" {
name = "prod-redis-sg"
description = "Redis traffic from application tier only"
vpc_id = data.aws_vpc.production.id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [data.aws_security_group.app_tier.id]
description = "Redis from app servers"
}
tags = {
Name = "prod-redis-sg"
Environment = "production"
}
}
resource "aws_elasticache_subnet_group" "redis" {
name = "prod-redis-subnet-group"
subnet_ids = data.aws_subnets.private_cache.ids
tags = {
Name = "prod-redis-subnet-group"
}
}
# 2. Parameter group with