AWS Databases

Amazon ElastiCache for Redis Hands-On: Caching Patterns, HA & Performance

Quick take: Amazon ElastiCache is a managed in-memory data store — you pick Redis OSS, Valkey or Memcached, and AWS runs the nodes, the replication, the failover, the patching and the backups while you get a microsecond-to-single-digit-millisecond key-value store sitting in your VPC on port 6379. You put it in front of a slower, more expensive system of record — an RDS or Aurora database, a DynamoDB table, an API — so that the hot 5% of reads never touch it. Done right, a cache turns a database melting at 90% CPU into one loafing at 20%, cuts p99 read latency from tens of milliseconds to under one, and drops your database bill because you stop paying for read replicas you only needed to survive traffic. Done wrong, it is a second stateful system that lies to your users (stale data), falls over under a thundering herd when a hot key expires, silently drops to a 0% hit rate after a node replacement, or returns OOM command not allowed because the eviction policy was noeviction and nobody set a TTL. This article takes the whole thing apart — engines, the cache-aside / write-through / write-behind patterns, Redis data structures, cluster mode, Multi-AZ failover, Serverless, eviction, security and the metrics that matter — then builds a private, encrypted, Multi-AZ Redis replication group hands-on, implements cache-aside against RDS, and measures the hit-rate jump, in both aws CLI and Terraform.

Almost every read-heavy application eventually hits the same wall: the database is the bottleneck. The same product page, the same user profile, the same leaderboard, the same feature-flag lookup gets read thousands of times a second, each one a round trip to a relational engine that has to parse SQL, plan the query, walk an index and serialize a row — tens of milliseconds and a chunk of CPU, over and over, for data that changed an hour ago. You can throw a bigger instance at it, or three read replicas, and you will spend real money to paper over the fact that you are asking a database to do a job an in-memory hash table does a thousand times faster. A cache is that hash table, run as a service.

This guide is the complete, decision-by-decision treatment of ElastiCache for Redis for someone who has to operate a cache in production, not just draw it on a whiteboard. We define the caching mental model and where ElastiCache sits between your app and its system of record. We go engine by engine (Redis OSS vs Valkey vs Memcached, and when each is right), pattern by pattern (cache-aside/lazy loading, write-through, write-behind, and TTL-based expiry with the staleness and stampede traps baked into each), structure by structure (strings, hashes, sorted sets for leaderboards, streams — and the caching job each one does), then topology by topology: cluster mode disabled vs enabled, replication + Multi-AZ + automatic failover, the new ElastiCache Serverless, eviction policies and maxmemory, the endpoints (primary, reader, configuration), security (VPC, subnet group, the 6379 security group, encryption in transit and at rest, Redis AUTH and RBAC users), and the CloudWatch metrics that tell you whether the cache is helping or hurting. Then we build one hands-on and prove the hit rate.

By the end you can pick an engine and a topology on purpose, write cache-aside code that does not lie to users or stampede the database, launch a Multi-AZ Redis replication group that is private, encrypted and authenticated, size a node from its metrics, and work a failure — a dead connection, a low hit rate, an eviction storm, a failover blip, an OOM — from symptom to fix. This maps to the caching and performance domains of SAA-C03 (Solutions Architect Associate), DVA-C02 (Developer Associate) and SOA-C02 (SysOps Administrator).

What problem this solves

The problem ElastiCache solves is your database is doing expensive, repetitive read work it should not have to do. A relational query for the same unchanged row is pure waste — CPU spent parsing, planning and fetching data that was identical the last thousand times. Under load that waste becomes the bottleneck: connection pools saturate, p99 latency climbs, and the only knobs the database gives you (a bigger instance, more read replicas) cost more every month and still cap out. A cache moves those hot reads to an in-memory store that answers in microseconds, so the database only sees the first read of each value and every write — a fraction of the traffic.

What breaks without understanding it is rarely the creation — a Redis cluster appears in ten minutes. It is everything a naive cache silently gets wrong. You cache a value with no TTL and never invalidate it, so users see an hour-old price forever. You set maxmemory-policy to the ElastiCache default of volatile-lru, forget to put TTLs on your keys, the node fills, and every write starts failing with OOM command not allowed because there is nothing evictable. A viral event expires a hot key and ten thousand concurrent requests all miss at once and stampede the database into the ground — the exact outage the cache was supposed to prevent. A node gets replaced during maintenance, the cache comes back empty, the hit rate craters to 0%, and the cold database gets the full firehose. You run a KEYS * in a debugging session and block the single-threaded Redis event loop for two seconds, and every user everywhere sees a latency spike. You point the app at the primary endpoint, a failover promotes a replica, the DNS moves, and because the client cached the IP it errors for a minute. Each is a specific, avoidable trap, and each is in this article.

Who hits this: every backend and platform engineer who has ever watched a database redline under read traffic. It bites hardest on teams adding their first cache (the staleness and stampede traps), on people who treat Redis like a database instead of a cache (the eviction and durability traps), on anyone running a leaderboard, session store or rate limiter (the data-structure choices), and on cost-optimizers who do not realize a three-node Multi-AZ replication group bills for all three nodes around the clock. Here is the whole field in one frame — the decisions every ElastiCache deployment forces you to make, whether or not you notice making them:

The caching decision The default if you don’t decide What it costs to get wrong Where in this article
Which engine? Whatever the wizard shows first Missing data structures, or paying for HA you don’t need Redis vs Memcached vs Valkey
Which caching pattern? Cache-aside, half-implemented Stale reads, stampedes, or double-writes Caching patterns
Cluster mode on or off? Disabled (single shard) A write/memory ceiling, or client CROSSSLOT errors Cluster mode
TTL — and jitter? No TTL, or one fixed TTL everywhere Permanent staleness, or a synchronized stampede Caching patterns / TTL
Eviction policy + node size? volatile-lru, undersized node OOM write failures or a low hit rate Eviction policies & maxmemory
Multi-AZ + failover? Single node, no replica Total cache loss + a cold-DB thundering herd on any AZ blip Replication, Multi-AZ & failover
Encryption + AUTH/RBAC? Off unless you tick it A wide-open cache reachable by anything in the VPC Security
Which metrics do you watch? CPUUtilization (the wrong one) You miss the real saturation signal (EngineCPUUtilization) Monitoring

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account, the AWS CLI v2 configured with a non-root IAM identity, and a VPC with at least two private subnets in different Availability Zones — a cache belongs on private subnets, reachable only from your app tier. If your networking is fuzzy, build the substrate first with Building an AWS VPC from Scratch: Subnets, Route Tables, IGW & a Public/Private Design. You should be comfortable at a shell, able to read a Terraform resource block, and know basic SQL — the cache sits in front of a database you already understand.

This sits in the Databases track, one layer above the database. It assumes you have already chosen and launched your system of record; the natural predecessor is Launch Amazon RDS (MySQL & PostgreSQL) Hands-On, whose private-subnet, security-group-only design is exactly the shape a cache wants too. If the pressure you are trying to relieve is a slow database rather than a read-volume problem, first confirm the queries themselves are tuned — RDS Performance Insights: Finding and Tuning Slow Queries — because caching a slow query hides it, it does not fix it. When your access pattern is key-value at massive scale, the system of record might be Amazon DynamoDB Tables, Keys & Capacity Hands-On instead of a relational engine — the cache-aside pattern here works the same in front of either. And the cache is one tier of a bigger picture: see where it plugs into The AWS Three-Tier Web Application Architecture.

Here is the assumed knowledge, why each piece matters specifically for running a cache, and where to shore it up:

You should know… Why it matters here Brush up
What a VPC, subnet and AZ are The cache needs a subnet group across ≥2 private AZs Building an AWS VPC from Scratch
How a security group works (stateful, allow-only) The SG on 6379 is the cache’s only firewall Launch Amazon RDS Hands-On
What a relational database costs per read It’s the pain the cache relieves RDS Performance Insights
Basic key-value thinking Redis is a key → value store, not tables This article, Core concepts
That a cache can serve stale data Every pattern trades freshness for speed This article, Caching patterns
Where a cache sits in a web app It’s the read-path accelerator between app and DB Three-Tier Web Application Architecture

Core concepts

Eight mental models make every later decision obvious.

A cache is a fast, lossy copy — never the source of truth. Redis holds a copy of data whose authoritative home is elsewhere (a database, an API). The cache can be wiped, expire, or fall behind, and your application must be correct anyway — every read that misses the cache must be answerable from the system of record. The instant you start treating Redis as the only place a piece of data lives, you have a database with none of a database’s durability guarantees, and one node replacement from data loss.

The whole value is the hit rate. A cache earns its keep only when reads hit it. A hit is answered from memory in microseconds and never touches the database; a miss costs a database round trip plus a cache write, so it is slightly slower than no cache at all. A cache at a 95% hit rate offloads 95% of reads; a cache at 30% is mostly overhead. Everything you tune — TTLs, key design, node size, eviction — is in service of the hit rate.

Redis is (mostly) single-threaded, and that is a feature and a trap. The Redis command loop runs commands one at a time on a single core, which is why every operation is atomic and why it is blindingly fast for small operations — no locks. It is also why one O(N) command on a big key (KEYS *, SMEMBERS on a million-member set, a huge DEL) blocks every other client for the duration. Multithreaded I/O exists in newer versions for network handling, but command execution is still effectively serial. Watch EngineCPUUtilization (the Redis thread), not CPUUtilization (the whole box), to see real saturation.

ElastiCache lives only inside your VPC. There is no public endpoint, ever. A cache node is reachable only from within its VPC (or a peered VPC / VPN / Transit Gateway). Reachability is entirely the attached security group on port 6379 (Redis/Valkey) or 11211 (Memcached). This is why “I can’t connect from my laptop” is almost always correct behaviour, not a bug.

A replication group is one or more shards, each a primary plus replicas. The ElastiCache unit for Redis is a replication group. With cluster mode disabled it is a single shard (node group) — one primary and up to five read replicas. With cluster mode enabled it is up to 500 shards, each its own primary+replicas, with the keyspace partitioned across them. Replication from primary to replica is asynchronous, so a replica can lag the primary by a few milliseconds.

Endpoints hide the nodes, and they move. You never connect to a node’s IP. Cluster mode disabled gives you a primary endpoint (a DNS name that always points at the current write node) and a reader endpoint (which load-balances across replicas for reads). Cluster mode enabled gives you a configuration endpoint the client uses to discover the whole slot map. On a failover the primary endpoint’s DNS is repointed — connect by name, never a cached IP.

Eviction is what makes it a cache, not a database. When a node reaches maxmemory, its eviction policy decides what to drop to make room. A pure cache should evict the least-useful keys (allkeys-lru/allkeys-lfu) and keep serving; a policy of noeviction turns a full cache into one that rejects writes with OOM command not allowed. The ElastiCache default is volatile-lru, which only evicts keys that have a TTL — a subtle trap if your keys have no TTL.

TTL bounds staleness but creates stampedes. A time-to-live on a key guarantees the cache can be at most that stale before the next read refetches. But if thousands of clients hold the same hot key and it expires at the same instant, they all miss at once and stampede the database — the thundering herd. The fixes are TTL jitter (randomize expiry so keys don’t all die together) and a rebuild lock (let one request repopulate while others wait or serve stale).

Here is every moving part of an ElastiCache-for-Redis deployment in one table, with who sets it and the gotcha that bites:

Building block What it is You set it… Gotcha
Engine + version Redis OSS / Valkey / Memcached, a version At create; upgrade later Engine choice constrains data structures, HA, cluster
Node type The VM size (cache.t4g.microcache.r7g.16xlarge) At create; modifiable cache.t burstable throttles; cache.r for memory
Cluster mode Single shard vs sharded (16,384 slots) At create (migratable, but plan it) Enabled needs a cluster-aware client; CROSSSLOT limits
Replicas per shard 0–5 read replicas At create; add/remove later 0 replicas = no failover, no read scaling
Multi-AZ + failover Replicas in other AZs, auto-promote At create; toggle later Needs ≥1 replica; failover is a ~30–60s blip
Cache subnet group Which subnets/AZs nodes may use At create Needs ≥2 private AZs for HA
Security group The inbound firewall on 6379 At create; editable 0.0.0.0/0 = every VPC workload can read your cache
Parameter group Engine config (maxmemory-policy, etc.) Attach at create; edit anytime Some params need a reboot; default policy is volatile-lru
maxmemory-policy What to evict when full Parameter group noevictionOOM; volatile-* needs TTLs
Encryption in transit TLS on 6379 At create only Required for AUTH; adds a tiny latency cost
Encryption at rest KMS on snapshots/backups/disk At create only No in-place toggle
AUTH token / RBAC Password or ACL users At create (RBAC editable) AUTH requires TLS; RBAC is the modern choice
Backups (snapshots) Point-in-time RDB copies At create; on demand Off by default; a snapshot briefly forks memory

Redis vs Memcached vs Valkey — choosing the engine

ElastiCache offers three engines, and the choice sets the ceiling on everything after it. Memcached is the classic simple cache: a multithreaded, volatile, key-value blob store with client-side sharding and no replication, no failover, no data structures and no persistence. Redis OSS is the rich one: single-threaded command execution, dozens of data structures, replication, Multi-AZ failover, cluster mode, pub/sub, transactions, Lua and snapshots. Valkey is the Linux Foundation’s open-source fork of Redis 7.2 (created after Redis Inc. relicensed Redis away from open source in 2024); ElastiCache added Valkey support and prices it lower than Redis OSS for the same capabilities, because it is wire- and feature-compatible.

The three engines, side by side

Dimension Memcached Redis OSS Valkey
Model Volatile key-value blobs Rich data-structure store Rich data-structure store (Redis fork)
Default port 11211 6379 6379
Threading Multithreaded Single-threaded command loop Single-threaded command loop
Data structures Strings only Strings, hashes, lists, sets, sorted sets, streams, bitmaps, HLL, geo Same as Redis
Replication / read replicas ❌ None ✅ Up to 5 per shard ✅ Up to 5 per shard
Multi-AZ / automatic failover
Cluster / sharding Client-side only ✅ Cluster mode (16,384 slots) ✅ Cluster mode
Persistence / backups ✅ Snapshots (RDB) ✅ Snapshots
Pub/Sub, transactions, Lua
Encryption in transit / at rest In-transit (newer) ✅ Both ✅ Both
AUTH / RBAC SASL (limited) ✅ AUTH + RBAC ✅ AUTH + RBAC
Serverless option ✅ (lowest cost)
Relative cost Low Standard ~Lower than Redis OSS
Best at Simple, huge, multi-core cache of opaque blobs Everything else Everything Redis does, cheaper

When to pick which

If you need… Choose Why
The most capable cache with data structures, HA, cluster Valkey (or Redis OSS) Full feature set; Valkey is the lower-cost default for new builds
Exact compatibility with an existing Redis OSS version/feature Redis OSS Match a version or a feature Valkey hasn’t reached yet
A dead-simple, multi-threaded cache of small opaque objects, no HA needed Memcached Multithreaded scale-out on big nodes, lowest complexity
Sorted sets for a leaderboard, streams for a queue, pub/sub Valkey / Redis OSS Memcached has none of these
Automatic failover and read replicas Valkey / Redis OSS Memcached has no replication at all
The lowest running cost for a new Redis-compatible workload Valkey AWS prices Valkey below Redis OSS for the same shape

For everything in this article we use Redis-compatible ElastiCache (Redis OSS or Valkey — the commands and Terraform are identical; set engine = "valkey" to save money). Memcached is a genuinely different service and out of scope beyond this comparison. The one-line rule: reach for Memcached only when you want a plain, shardable, multi-core blob cache with no replication; reach for Valkey/Redis for literally everything else.

Caching patterns: cache-aside, write-through, write-behind, TTL

A cache is only as good as the pattern you wrap around it — the rules for when you read it, when you write it, and how data stays consistent with the source. There are three canonical write patterns and one expiry mechanism, and most real systems combine them.

The patterns compared

Pattern Who writes the cache On read Consistency Write latency Failure mode
Cache-aside (lazy loading) The app, after a miss Check cache → miss → read DB → populate Eventually consistent; stale until TTL/invalidation Low (write path untouched) Cold cache; stale reads; stampede on expiry
Read-through The cache library, on a miss Cache fetches from DB itself Same as cache-aside Low Same, hidden inside the client library
Write-through The app, on every write, synchronously Cache is already warm Cache never stale (for written keys) Higher (write cache + DB) Caches rarely-read data; cold on new keys
Write-behind (write-back) The app writes cache; cache flushes to DB async Cache is warm Cache leads DB; DB eventually consistent Lowest write latency Data loss if cache dies before flush

Cache-aside (lazy loading) — the default, and its read-miss flow

Cache-aside is the pattern you will use 90% of the time. The application, not the cache, owns the logic: on a read it checks Redis first; on a hit it returns immediately; on a miss it reads the database, writes the value back into Redis with a TTL, and returns it. Only data that is actually requested is ever cached (lazy loading), which keeps the cache small and hot. The trade-offs are a cold start (the cache is empty until requests warm it), a staleness window (a cached value can be up to its TTL out of date after the DB changes), and the cache stampede on expiry.

Step What happens Cost
1 App computes the cache key (e.g. product:42)
2 GET product:42 ~sub-ms
3a Hit: value returned, done Fast path, no DB
3b Miss: returns nil ~sub-ms
4 On miss: query the DB (SELECT … WHERE id=42) ~ms–tens of ms
5 On miss: SET product:42 <json> EX 300 (populate + TTL) ~sub-ms
6 Return the value to the caller

On a write, cache-aside must invalidate (delete) or update the key, or readers keep the stale copy until the TTL. The common bug is forgetting step-on-write, so the cache serves old data for the whole TTL after every update.

Write-through — freshness at the cost of write latency

Write-through writes the cache and the database together on every write, so a subsequently-read key is always warm and never stale. The cost is on the write path (two writes instead of one) and in memory (you cache data that may never be read). It shines for data that is written once and read constantly, and it is usually combined with cache-aside so that keys not yet written are still lazily loaded and a cache flush can be repopulated.

Concern Cache-aside Write-through
Read latency (warm) Low Low
Read latency (cold key) High (DB fetch) Low (already written)
Write latency Unchanged Higher (cache + DB)
Staleness Up to TTL after a DB change None for written keys
Memory efficiency High (only read data cached) Lower (writes rarely-read data too)
Best for Read-heavy, tolerant of small staleness Write-then-read-many, strong freshness

Write-behind and TTL

Write-behind (write-back) writes only to the cache and flushes to the database asynchronously, giving the lowest possible write latency at the price of durability — if the cache node dies before the flush, those writes are lost. ElastiCache has no built-in write-behind; you would implement it at the application or with a stream + consumer, and you should only do so for data you can afford to lose or reconstruct. For most teams it is a pattern to know, not to use.

TTL (time-to-live) is the safety net under every pattern: set an expiry on each key so the cache self-heals toward the source even if you miss an invalidation. Two rules matter. First, jitter the TTL (base + random(0, spread)) so a batch of keys written together doesn’t all expire together and stampede the DB. Second, size the TTL to your tolerance for staleness and your write rate — short TTLs cut staleness but lower the hit rate (more misses), long TTLs raise the hit rate but widen the staleness window.

TTL strategy How When Trade-off
Fixed TTL EX 300 on every key Uniform, low-stakes data Synchronized expiry → stampede risk
TTL + jitter EX 300 + rand(0,60) The safe default Slightly variable freshness
No TTL + explicit invalidation DEL on write Data that changes rarely and you control every write path A missed invalidation = permanent staleness
Sliding TTL Reset TTL on access Session stores A hot key can live forever
Early recompute / soft TTL Refresh before hard expiry Very hot keys More complexity in the app
# Cache-aside by hand with redis-cli (TLS + AUTH): miss, populate with a jittered TTL, hit
redis-cli -h $PRIMARY -p 6379 --tls -a "$AUTH_TOKEN" GET product:42            # (nil) -> miss
redis-cli -h $PRIMARY -p 6379 --tls -a "$AUTH_TOKEN" SET product:42 '{"id":42,"price":999}' EX 330  # populate, ~300s+jitter
redis-cli -h $PRIMARY -p 6379 --tls -a "$AUTH_TOKEN" GET product:42            # JSON -> hit

Redis data structures and their caching jobs

Memcached caches opaque blobs; Redis caches structures, and choosing the right one turns a hard problem (a real-time leaderboard, a sliding-window rate limiter) into a two-line operation. This is the single biggest reason to pick Redis/Valkey over Memcached.

The structures and what they’re for

Structure Key commands Caching job it does best
String GET/SET/INCR/SETEX Cached JSON/HTML, counters, rate-limit tokens, flags
Hash HSET/HGET/HGETALL Objects with fields — a user profile or session as one key
List LPUSH/RPOP/LRANGE Simple queues, recent-activity feeds, timelines
Set SADD/SISMEMBER/SINTER Unique membership, tags, dedup, “who’s online”
Sorted set (ZSET) ZADD/ZRANGE/ZINCRBY/ZREVRANK Leaderboards, priority queues, sliding-window rate limits, time-series
Stream XADD/XREAD/XREADGROUP Durable event log / message queue with consumer groups
Bitmap SETBIT/BITCOUNT Daily-active flags, presence, feature bitmaps (tiny memory)
HyperLogLog PFADD/PFCOUNT Approximate unique counts (uniques/day) in ~12 KB
Geospatial GEOADD/GEOSEARCH “Nearest N” proximity queries

Use case → structure → why

Use case Structure Why it fits
Session store Hash (or String of JSON) One key per session, fields for attributes, sliding TTL
DB read-offload String (JSON) Cache the serialized row/result under a stable key, TTL it
Leaderboard Sorted set ZINCRBY to score, ZREVRANGE 0 9 for the top 10 — O(log N)
Rate limiting String INCR + EXPIRE, or ZSET Fixed-window counter, or sliding-window with sorted timestamps
Pub/Sub fan-out Pub/Sub channels Push notifications/events to many subscribers (fire-and-forget)
Work queue List or Stream LPUSH/BRPOP, or Streams with consumer groups + acks
Deduplication Set or HyperLogLog Exact membership, or approximate cardinality at scale
Distributed lock String SET key val NX PX The rebuild lock that stops a cache stampede

The lesson: before you cache a JSON blob and call it done, ask whether a native structure does the job better. A leaderboard built on a sorted set is a handful of O(log N) commands; the same thing on a relational ORDER BY … LIMIT over a hot table is a recurring performance incident.

Cluster mode: disabled vs enabled

A Redis replication group runs in one of two topologies, and the choice determines your scaling ceiling and how the client must behave.

Disabled vs enabled

Aspect Cluster mode disabled Cluster mode enabled
Shards (node groups) Exactly 1 1–500
Nodes per shard 1 primary + 0–5 replicas 1 primary + 0–5 replicas each
Where data lives All keys on the one primary Partitioned across 16,384 hash slots
Scale writes / memory Vertically (bigger node) Horizontally (add shards)
Scale reads Add replicas / reader endpoint Replicas per shard
Client Any Redis client Must be cluster-aware (follows MOVED/ASK)
Multi-key ops Any keys Only within one slot (else CROSSSLOT)
Endpoint Primary + reader endpoints Configuration endpoint
Max data One node’s memory (up to ~635 GiB on big nodes) Sum across shards (many TiB)
When to use Most workloads; simplest Data or write throughput exceeds one node

Hash slots and the client difference

In cluster mode, every key maps to one of 16,384 hash slots via CRC16(key) mod 16384, and each shard owns a contiguous range of slots. A cluster-aware client caches the slot→shard map from the configuration endpoint and talks directly to the right shard; if it targets the wrong one, the shard replies MOVED (permanently moved, update your map) or ASK (temporarily, during a resharding). A multi-key command (MGET a b c, a transaction) only works if all keys are in the same slot, or you get CROSSSLOT Keys in request don't hash to the same slot. The fix is a hash tag: wrap the part of the key you want to co-locate in braces — user:{123}:profile and user:{123}:cart both hash on 123 and land on the same slot.

Cluster concept Rule / value Why it matters
Total hash slots 16,384 (fixed) The unit of data placement and resharding
Slot of a key CRC16(key) mod 16384 Deterministic; same key → same slot always
Hash tag {...} substring hashes the key Co-locate related keys to allow multi-key ops
MOVED redirect Slot lives on another shard (permanent) Client updates its slot map and retries
ASK redirect Slot is migrating (temporary) Client retries on the new shard for that key only
CROSSSLOT error Multi-key op spans slots Use hash tags, or split the operation
Resharding Move slots between shards online Add/remove shards without downtime (brief ASKs)
# Talk to a cluster-mode-enabled cache: -c makes redis-cli follow MOVED/ASK redirects
redis-cli -c -h $CONFIG_ENDPOINT -p 6379 --tls -a "$AUTH_TOKEN" SET user:{123}:cart '...'
redis-cli    -h $CONFIG_ENDPOINT -p 6379 --tls -a "$AUTH_TOKEN" CLUSTER SLOTS   # inspect the slot map

Replication, Multi-AZ & automatic failover

High availability in ElastiCache-for-Redis is built from replicas and Multi-AZ. A shard has one primary and 0–5 read replicas; the primary asynchronously streams its changes to the replicas. Turn on Multi-AZ with automatic failover and ElastiCache places replicas in different Availability Zones and, if the primary fails its health checks, promotes a replica to primary and repoints the primary endpoint’s DNS at it — no manual intervention.

The HA building blocks

Concept What it is Rule / gotcha
Primary node The single writable node in a shard All writes go here; replication is async
Read replica A read-only copy of the primary 0–5 per shard; serves reads; can be promoted
Replication lag How far a replica trails the primary Async → a replica read can be a few ms stale
Multi-AZ Replicas in other AZs Needs ≥1 replica; survives an AZ outage
Automatic failover Auto-promote a replica on primary failure ~30–60s detection + DNS repoint
Failover blip The window during a failover Writes error briefly; clients must reconnect

Failover behaviour, and what the client must do

Failover aspect Behaviour
Trigger Primary fails health checks (hardware, AZ, or a planned replacement)
Detection + promote Typically under ~60 seconds end to end
Endpoint Primary endpoint DNS is repointed to the new primary automatically
In-flight writes Writes during the window fail; the app must retry
READONLY errors Briefly, a replica about to be promoted may reject writes
Client duty Connect by endpoint DNS (never a cached IP), reconnect on error, retry idempotent writes
Reader endpoint Keeps serving reads across the remaining replicas throughout

Endpoints — primary, reader, configuration

You connect by an endpoint, and which ones you get depends on the topology. Using the wrong endpoint is a common cause of READONLY errors (writing to a reader) and of failovers that “don’t work” (a client pinned to a node IP).

Endpoint Topology Points at Use it for
Primary endpoint Cluster mode disabled The current primary (moves on failover) All writes and strongly-consistent reads
Reader endpoint Cluster mode disabled Load-balanced across replicas Scaling reads; tolerate a few ms of lag
Configuration endpoint Cluster mode enabled The whole cluster (slot map) Cluster-aware clients discover all shards
Node endpoint Any One specific node Debugging only — never hard-code it in an app
# Grab the primary and reader endpoints of a cluster-mode-disabled replication group
aws elasticache describe-replication-groups --replication-group-id kv-lab-redis \
  --query 'ReplicationGroups[0].NodeGroups[0].[PrimaryEndpoint.Address,ReaderEndpoint.Address]' \
  --output text

ElastiCache Serverless

ElastiCache Serverless removes node sizing entirely: you create a cache, get a single endpoint, and it scales capacity up and down automatically with your traffic — no node type, no shard count, no replica math. It is Multi-AZ and TLS-encrypted by default. Billing changes shape too: instead of paying per node-hour, you pay for data stored (GB-hour) and ElastiCache Processing Units (ECPUs) consumed per request. It is the right default when your traffic is spiky or unpredictable, or when you simply do not want to manage capacity; a carefully-sized node-based cluster can still be cheaper for a steady, well-understood, high-throughput workload.

Dimension Node-based (self-designed) Serverless
You choose Node type, shards, replicas, AZs Nothing — just create it
Scaling Manual (resize, add shards/replicas) Automatic, near-instant
High availability You enable Multi-AZ + replicas Multi-AZ by default
Encryption You enable in-transit/at-rest On by default
Billing Per node-hour (all nodes) Data stored (GB-hour) + ECPUs
Minimum cost One node running 24×7 A small storage floor + usage
Best for Steady, predictable, tuned throughput Spiky/unknown load, zero-ops
Endpoint Primary/reader/configuration One endpoint
Serverless concept Note
ECPU Unit of request work billed; roughly scales with commands × data touched
Data stored Billed per GB-hour with a small minimum floor (lower on Valkey)
Scaling signal Tracks throughput and memory; no pre-provisioning
Cluster semantics Sharded under the hood; use hash tags for multi-key ops
When it’s cheaper Bursty or low average utilization where a 24×7 node would idle
When node-based wins Sustained high throughput you can size a cache.r for precisely

Eviction policies & maxmemory

Each node has a maxmemory (set by its node type, minus a reserved slice), and the maxmemory-policy parameter decides what happens when it fills. This is the single setting most often left wrong, and it is the difference between a cache that quietly evicts cold keys and one that starts rejecting writes.

The policies

Policy What it evicts Reach for it when Gotcha
noeviction Nothing — writes fail with OOM You use Redis as a durable store, not a cache A full cache stops accepting writes
allkeys-lru Least-recently-used across all keys General-purpose cache (recommended) Evicts anything, including no-TTL keys
allkeys-lfu Least-frequently-used across all keys Skewed hot/cold access patterns Slightly more bookkeeping
allkeys-random A random key Uniform access, rarely useful Ignores usefulness
volatile-lru LRU among keys with a TTL Mixed cache + persistent keys ElastiCache default — no TTLs → OOM
volatile-lfu LFU among keys with a TTL Same, frequency-based Same TTL caveat
volatile-random Random among TTL keys Rare Same TTL caveat
volatile-ttl The key nearest its expiry Prefer to drop soonest-expiring Same TTL caveat

The trap in bold: ElastiCache defaults to volatile-lru, which only ever evicts keys that carry a TTL. If your application caches keys without a TTL (easy to do), the node fills, volatile-lru finds nothing evictable, and every write fails with OOM command not allowed. For a pure cache, set allkeys-lru (or allkeys-lfu) so any key is fair game.

maxmemory and reserved memory

Setting What it does Default / note
maxmemory The memory ceiling before eviction Set automatically from the node type
reserved-memory-percent Memory held back for overhead (failover, backup fork, replication buffers) Default 25%; don’t set it to 0
maxmemory-policy Eviction policy (above) Default volatile-lru; use allkeys-lru for a cache
maxmemory-samples How many keys LRU/LFU samples per eviction Default 5; higher = more accurate, more CPU
# Create a parameter group that makes eviction sane for a cache, then use it on the group
aws elasticache create-cache-parameter-group \
  --cache-parameter-group-name kv-redis7-cache \
  --cache-parameter-group-family redis7 \
  --description "KloudVin lab: cache-friendly eviction"

aws elasticache modify-cache-parameter-group \
  --cache-parameter-group-name kv-redis7-cache \
  --parameter-name-values "ParameterName=maxmemory-policy,ParameterValue=allkeys-lru"
resource "aws_elasticache_parameter_group" "cache" {
  name   = "kv-redis7-cache"
  family = "redis7"

  parameter {
    name  = "maxmemory-policy"
    value = "allkeys-lru"   # a cache should evict, not OOM
  }
}

Security: VPC, subnet group, the 6379 SG, encryption, AUTH & RBAC

A cache is often overlooked in a threat model, yet it holds copies of your hottest data — sessions, profiles, tokens. ElastiCache security is layered, and most of it is decided at create time.

The layers

Layer Control What it stops
Network reachability In-VPC only; no public endpoint Anything outside the VPC
Cache subnet group Private subnets across ≥2 AZs Placement in a public subnet
Security group (6379) Allow only the app SG Every other VPC workload reading the cache
Encryption in transit TLS on 6379 Sniffing on the wire; required for AUTH
Encryption at rest KMS on snapshots/backups Data exposure from a snapshot
AUTH token / RBAC Password or ACL users Unauthenticated access from within the VPC
IAM Control-plane permissions Who can create/modify/delete/snapshot the cache

The security group — the one firewall

ElastiCache has no inbound rules of its own; reachability is the attached security group. The correct rule is TCP 6379 sourced from the application’s security group id, never a CIDR and never 0.0.0.0/0. Because the cache is in-VPC-only, 0.0.0.0/0 doesn’t expose it to the internet — but it does let every workload in the VPC read and write your cache, which for a session or token store is a real problem.

Direction Port Protocol Source Purpose
Inbound 6379 TCP App SG id (sg-app) App → cache only
Inbound 6379 TCP Bastion/SSM host SG (lab) Admin redis-cli
Outbound all all default (SG is stateful) Return traffic
NEVER 6379 TCP 0.0.0.0/0 Every VPC workload can read the cache

Encryption in transit and at rest

Encryption Covers Set when Note
In transit (TLS) Client↔node and node↔node traffic on 6379 Create only Required to use an AUTH token; connect with --tls
At rest (KMS) On-disk data, snapshots, backups Create only aws/elasticache key or your own CMK

AUTH token vs RBAC

Two ways to authenticate, and RBAC is the modern one. The legacy AUTH token is a single shared password (16–128 chars) every client presents; it requires in-transit encryption. RBAC (Role-Based Access Control), on Redis 6+ and Valkey, defines users with an access string (an ACL of allowed commands and key patterns) grouped into a user group attached to the replication group — so you can give the app a user that can only GET/SET on app:* and an ops user that can run INFO but not FLUSHALL.

Aspect AUTH token RBAC
Granularity One shared password Per-user, per-command, per-key-pattern ACL
Least privilege ❌ All-or-nothing ✅ Scope commands and keys
Rotation Two-phase ROTATE (set two tokens, then remove old) Update the user’s password
Requires TLS ✅ Yes Recommended
Engine Redis 4+/Valkey Redis 6+/Valkey
Use when Simplest single-app cache Anything shared or compliance-sensitive
# Rotate an AUTH token without downtime: SET a second token, roll clients, then remove the old one
aws elasticache modify-replication-group --replication-group-id kv-lab-redis \
  --auth-token 'NewStr0ng-Token-2026!' --auth-token-update-strategy ROTATE --apply-immediately
# ...after all clients use the new token:
aws elasticache modify-replication-group --replication-group-id kv-lab-redis \
  --auth-token 'NewStr0ng-Token-2026!' --auth-token-update-strategy SET --apply-immediately

Monitoring: the metrics that matter

You cannot tune what you cannot see, and ElastiCache emits a lot of CloudWatch metrics. These are the ones that actually tell you whether the cache is healthy, saturated, or hurting — with the single most common mistake being to watch CPUUtilization (the whole host, misleading on single-threaded Redis) instead of EngineCPUUtilization (the Redis thread).

Metric What it tells you Watch for Do this
CacheHitRate % of reads served from cache Low or falling Fix TTLs/key design; warm the cache; bigger node
CacheHits / CacheMisses Raw hit/miss counts Misses dominating Same as above
Evictions Keys evicted at maxmemory Rising / non-zero under pressure Bigger node, add shards, or check the policy
EngineCPUUtilization The Redis thread CPU > ~90% sustained Scale up, shard, or kill O(N) commands/big keys
CPUUtilization Whole-host CPU (all cores) Less useful for Redis Use EngineCPUUtilization instead
DatabaseMemoryUsagePercentage % of maxmemory used Near 100% Bigger node/shards, or expect evictions/OOM
SwapUsage Memory swapped to disk > 0 and growing Memory pressure — resize; never let it swap
FreeableMemory Reclaimable RAM on the node Trending to zero Same — the node is running out of room
CurrConnections Open client connections Near the limit; unbounded growth Pool connections; fix leaks
NewConnections Connection churn per interval High churn Reuse connections; you’re paying handshake cost
ReplicationLag Seconds a replica trails the primary High/growing Reader reads are stale; check replica load
NetworkBytesIn/Out Bandwidth Near the node’s cap Bigger node with more network
CurrItems Number of keys stored Unexpected growth Missing TTLs; a leak

The three you alert on first: CacheHitRate (is the cache working?), Evictions + DatabaseMemoryUsagePercentage together (is it too small?), and EngineCPUUtilization (is the single thread saturated?). Everything else is context.

Architecture at a glance

Read the diagram left to right as the real, private, cache-aside request path you are about to build. An application in the VPC — on EC2 or Lambda — opens a TLS connection on 6379 to the ElastiCache primary, through a security group that admits 6379 only from the app’s own SG, into a cache subnet group spanning two private Availability Zones. On a hit, the value returns in sub-millisecond time and the database is never touched. On a miss, the app reads the system of record — an RDS database or a DynamoDB table — writes the value back into Redis with a TTL plus jitter, and returns it, so the next reader hits the cache. A reader replica in the second AZ serves read traffic and is promoted automatically if the primary fails. The connection is authenticated with an AUTH token or an RBAC user and the data is encrypted at rest. Each numbered badge marks a decision or a failure class, and the legend narrates each as symptom, how to confirm, and the fix.

Amazon ElastiCache for Redis cache-aside architecture: an application on EC2 or Lambda connects over TLS on port 6379 through a security group that allows only the app security group, into a Redis replication group with a primary and a reader replica across two private Availability Zones with automatic failover; on a cache hit the value returns immediately, and on a miss the app reads an RDS or DynamoDB system of record and populates Redis with a jittered TTL, with an AUTH token or RBAC user and at-rest KMS encryption securing the cache

Real-world scenario

Kirana Cart is a fictional Indian grocery-delivery startup running a Django app on ECS Fargate against a db.r6g.large PostgreSQL RDS instance. For a year they had no cache. Every product page rendered ran the same handful of queries — the product row, its price, its stock, the category tree — thousands of times a minute during the evening rush, and the database sat at 80–90% CPU by 7 p.m. every day. They had already added two read replicas (≈$400/month) and were about to add a third when a new architect asked the obvious question: why is the database re-reading a product that changes once a week, ten thousand times an hour?

The first cache made things worse — briefly. The team dropped in a cache.t3.micro Redis node, cache-aside, with a flat 60-second TTL on every key and the ElastiCache default maxmemory-policy of volatile-lru. Three things went wrong within a week. First, the tiny 0.5 GiB node filled almost immediately; because the app set TTLs on product keys but cached the category tree without one, volatile-lru had plenty to evict and the hit rate on categories was near zero — constant misses. Second, the flat 60-second TTL meant that at the top of every minute a wave of hot product keys expired together, and the resulting thundering herd hammered the database with synchronized misses — the CPU graph grew a spike every 60 seconds. Third, when the single node was replaced during a maintenance window, the cache came back empty and the cold database briefly took the full firehose.

The rebuild is exactly this article’s lab. They moved to a Multi-AZ replication group: a cache.r6g.large primary (13+ GiB of memory — room for the whole hot set) plus one replica in a second AZ, automatic failover on, allkeys-lru eviction so nothing could OOM, TLS + an RBAC user scoped to GET/SET on product:* and category:*, and a base 300s TTL with up to 60s of random jitter so keys stopped expiring in lockstep. The category tree got a TTL and an explicit invalidation on the (rare) catalog update. Reads went to the reader endpoint, writes and invalidations to the primary endpoint. They added a SET NX rebuild lock so that when a genuinely hot key expired, one request repopulated it while the rest briefly served the previous value.

The result: the CacheHitRate climbed to 96%, database read load dropped by roughly 90%, and the two read replicas came out (a $400/month saving that more than paid for the cache). Evening database CPU fell from 85% to under 20%, p99 product-page latency dropped from ~40 ms to ~3 ms, and the per-minute CPU spikes vanished once the TTL jitter spread the expiries. The lesson the architect repeated: a cache is not a checkbox — the node size, the eviction policy, the TTL jitter and the stampede lock are the difference between a cache that saves you money and one that quietly hands the database a fresh outage every minute.

Advantages and disadvantages

Advantages Disadvantages
Microsecond-to-single-ms reads offload the hottest traffic A second stateful system to run, secure and monitor
Managed nodes, patching, replication, failover, backups Cache-aside/invalidation logic is your code to get right
Rich data structures turn hard problems into O(log N) commands Redis is single-threaded — one big key or KEYS hurts everyone
Multi-AZ automatic failover is a toggle, not a project Failover is a ~30–60s blip clients must handle
Cuts database cost (fewer/smaller read replicas) A three-node Multi-AZ group bills for all three nodes 24×7
Serverless removes capacity planning entirely Serverless can cost more than a well-sized node at steady load
RBAC + TLS + at-rest encryption for a real security posture Encryption and cluster mode are effectively create-time decisions
Stale data is tunable via TTL and invalidation Stale data is possible — every cache trades freshness for speed

A cache wins whenever the same data is read far more often than it changes and the read is expensive — which is most read-heavy web workloads. It loses, or hurts, when you cache write-heavy or unique-per-request data (low hit rate, pure overhead), when you treat it as durable (it is not), or when you skip the discipline — TTL jitter, sane eviction, stampede protection, right-sized nodes — that separates a cache that helps from one that adds a failure mode.

Hands-on lab

You will build a private, encrypted, Multi-AZ Redis replication group (cluster mode disabled: one primary + two replicas), lock it to port 6379 from an app security group, force TLS and an AUTH token, set allkeys-lru eviction, then implement cache-aside from a client against a source database and measure the hit-rate jump — then tear it all down. Everything is aws CLI first, with a complete Terraform stack after. This is close to free-tier-friendly with a single cache.t3.micro, but ⚠️ a Multi-AZ three-node group, KMS, backups and cross-AZ data transfer all cost money — do the teardown.

Assumptions: AWS CLI v2 configured, a VPC (vpc-...) with two private subnets in different AZs, an app/admin EC2 host in that VPC with redis-cli (or an SSM session to one), and the app’s security group id. Export a few variables:

export AWS_REGION=us-east-1
export VPC_ID=vpc-0abc123def456
export SUBNET_A=subnet-0aaa11112222      # private, AZ a
export SUBNET_B=subnet-0bbb33334444      # private, AZ b
export APP_SG=sg-0app00appappapp         # SG worn by your app/admin host
export AUTH_TOKEN='Kv-L@b-Redis-Str0ng-Token-2026'   # 16-128 chars

Step 1 — cache subnet group (≥2 private AZs)

aws elasticache create-cache-subnet-group \
  --cache-subnet-group-name kv-lab-redis-subnets \
  --cache-subnet-group-description "Private subnets for the lab Redis" \
  --subnet-ids "$SUBNET_A" "$SUBNET_B"

Expected: JSON describing the subnet group with two subnets in different AZs.

Step 2 — a security group allowing 6379 from the app SG only

REDIS_SG=$(aws ec2 create-security-group --group-name kv-lab-redis-sg \
  --description "ElastiCache Redis lab SG" --vpc-id "$VPC_ID" --query GroupId --output text)

aws ec2 authorize-security-group-ingress \
  --group-id "$REDIS_SG" --protocol tcp --port 6379 \
  --source-group "$APP_SG"
echo "REDIS_SG=$REDIS_SG"

Expected: a sg-... id and an ingress rule allowing TCP 6379 from the app SG only. No 0.0.0.0/0 anywhere.

Step 3 — a cache-friendly parameter group (allkeys-lru)

aws elasticache create-cache-parameter-group \
  --cache-parameter-group-name kv-redis7-cache \
  --cache-parameter-group-family redis7 \
  --description "KloudVin lab: cache-friendly eviction"

aws elasticache modify-cache-parameter-group \
  --cache-parameter-group-name kv-redis7-cache \
  --parameter-name-values "ParameterName=maxmemory-policy,ParameterValue=allkeys-lru"

Expected: the group is created and maxmemory-policy is set to allkeys-lru — so a full cache evicts instead of returning OOM.

Step 4 — create the Multi-AZ replication group (TLS + AUTH + at-rest)

One primary and two replicas, automatic failover, Multi-AZ, encryption in transit and at rest, an AUTH token, on the parameter group from Step 3:

aws elasticache create-replication-group \
  --replication-group-id kv-lab-redis \
  --replication-group-description "KloudVin lab cache-aside Redis" \
  --engine redis --engine-version 7.1 \
  --cache-node-type cache.t3.micro \
  --num-cache-clusters 3 \
  --automatic-failover-enabled \
  --multi-az-enabled \
  --cache-subnet-group-name kv-lab-redis-subnets \
  --security-group-ids "$REDIS_SG" \
  --cache-parameter-group-name kv-redis7-cache \
  --transit-encryption-enabled \
  --at-rest-encryption-enabled \
  --auth-token "$AUTH_TOKEN" \
  --port 6379 \
  --snapshot-retention-limit 1

Expected: JSON with "Status": "creating". Note the flags doing the real work: --num-cache-clusters 3 (1 primary + 2 replicas), --automatic-failover-enabled + --multi-az-enabled (HA), --transit-encryption-enabled (TLS, required for AUTH), --at-rest-encryption-enabled and --auth-token. (Set --engine valkey and --engine-version 8.0 to run the cheaper Valkey instead — everything else is identical.)

Wait for it (5–10 minutes) and grab the endpoints:

aws elasticache wait replication-group-available --replication-group-id kv-lab-redis

PRIMARY=$(aws elasticache describe-replication-groups --replication-group-id kv-lab-redis \
  --query 'ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address' --output text)
READER=$(aws elasticache describe-replication-groups --replication-group-id kv-lab-redis \
  --query 'ReplicationGroups[0].NodeGroups[0].ReaderEndpoint.Address' --output text)
echo "PRIMARY=$PRIMARY"; echo "READER=$READER"

Step 5 — connect over TLS with the AUTH token

From the app/admin host inside the VPC:

redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" PING

Expected: PONG. If you get NOAUTH Authentication required you dropped -a; if it hangs, you’re not in the VPC or the SG doesn’t allow your host on 6379.

Step 6 — implement cache-aside and measure the hit rate

Reset the stats, then simulate a read pattern: the first read of a key misses (and populates), subsequent reads hit. Here is cache-aside by hand, then the Python the app would run:

redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" CONFIG RESETSTAT

# 1st read: miss -> populate with a jittered TTL (~300s + up to 60s)
redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" GET product:42          # (nil)
redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" SET product:42 '{"id":42,"price":999}' EX 342
# next 100 reads: hits
for i in $(seq 1 100); do
  redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" GET product:42 >/dev/null
done

redis-cli -h "$PRIMARY" -p 6379 --tls -a "$AUTH_TOKEN" INFO stats | grep -E 'keyspace_(hits|misses)'

Expected: keyspace_hits:100 and keyspace_misses:1 — a 99% hit rate for this key. In CloudWatch, CacheHitRate climbs as real traffic warms the cache. The application version of the same logic:

import json, os, random, redis, psycopg2

r = redis.Redis(host=os.environ["REDIS_PRIMARY"], port=6379, ssl=True,
                password=os.environ["REDIS_AUTH"], decode_responses=True,
                socket_timeout=1, socket_connect_timeout=1)

def get_product(pid: int) -> dict:
    key = f"product:{pid}"
    cached = r.get(key)                       # 1) check cache
    if cached is not None:
        return json.loads(cached)             #    HIT
    row = query_rds(pid)                       # 2) MISS -> system of record (RDS)
    ttl = 300 + random.randint(0, 60)          # 3) jittered TTL to avoid a stampede
    r.set(key, json.dumps(row), ex=ttl)        # 4) populate
    return row

def invalidate_product(pid: int) -> None:
    r.delete(f"product:{pid}")                 # on write: invalidate so readers refetch

For a genuinely hot key, guard the rebuild with a lock so a stampede repopulates once:

def get_hot(pid: int) -> dict:
    key = f"product:{pid}"
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached)
    lock = f"lock:{key}"
    if r.set(lock, "1", nx=True, ex=5):        # only one worker rebuilds
        try:
            row = query_rds(pid)
            r.set(key, json.dumps(row), ex=300 + random.randint(0, 60))
            return row
        finally:
            r.delete(lock)
    return query_rds(pid)                       # others: brief fallback to DB (or serve stale)

Step 7 — (optional) watch a failover

Trigger a test failover and confirm the primary endpoint moves without you changing the connection string:

aws elasticache test-failover --replication-group-id kv-lab-redis \
  --node-group-id 0001
# Reconnect via $PRIMARY (same DNS name) — it now resolves to the promoted node.

Expected: an ElastiCache Failover event; redis-cli -h "$PRIMARY" … PING still returns PONG after a brief reconnect. This is the blip your client must tolerate.

Step 8 — teardown (⚠️ removes everything)

# Delete the replication group (skip a final snapshot for a throwaway lab)
aws elasticache delete-replication-group --replication-group-id kv-lab-redis
aws elasticache wait replication-group-deleted --replication-group-id kv-lab-redis

# Clean up the leftovers
aws elasticache delete-cache-parameter-group --cache-parameter-group-name kv-redis7-cache
aws elasticache delete-cache-subnet-group --cache-subnet-group-name kv-lab-redis-subnets
aws ec2 delete-security-group --group-id "$REDIS_SG"

Expected: each resource deletes. Manual snapshots (if you took any) are not removed with the group — delete them explicitly or they bill.

The whole thing as Terraform

terraform {
  required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } }
}
provider "aws" { region = "us-east-1" }

variable "vpc_id"             { type = string }
variable "private_subnet_ids" { type = list(string) }   # two, different AZs
variable "app_sg_id"          { type = string }
variable "auth_token"         { type = string, sensitive = true }

resource "aws_elasticache_subnet_group" "redis" {
  name       = "kv-lab-redis-subnets"
  subnet_ids = var.private_subnet_ids
}

resource "aws_security_group" "redis" {
  name   = "kv-lab-redis-sg"
  vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "redis_from_app" {
  security_group_id            = aws_security_group.redis.id
  from_port                    = 6379
  to_port                      = 6379
  ip_protocol                  = "tcp"
  referenced_security_group_id = var.app_sg_id   # source is the app SG, not a CIDR
}

resource "aws_elasticache_parameter_group" "cache" {
  name   = "kv-redis7-cache"
  family = "redis7"
  parameter {
    name  = "maxmemory-policy"
    value = "allkeys-lru"        # evict, never OOM
  }
}

resource "aws_elasticache_replication_group" "redis" {
  replication_group_id = "kv-lab-redis"
  description          = "KloudVin lab cache-aside Redis"

  engine         = "redis"        # or "valkey" (cheaper)
  engine_version = "7.1"
  node_type      = "cache.t3.micro"
  port           = 6379

  num_cache_clusters         = 3           # 1 primary + 2 replicas (cluster mode disabled)
  automatic_failover_enabled = true
  multi_az_enabled           = true

  subnet_group_name  = aws_elasticache_subnet_group.redis.name
  security_group_ids = [aws_security_group.redis.id]
  parameter_group_name = aws_elasticache_parameter_group.cache.name

  transit_encryption_enabled = true        # TLS (required for auth_token)
  at_rest_encryption_enabled = true        # KMS
  auth_token                 = var.auth_token

  snapshot_retention_limit = 1
  apply_immediately        = true
}

output "primary_endpoint" { value = aws_elasticache_replication_group.redis.primary_endpoint_address }
output "reader_endpoint"  { value = aws_elasticache_replication_group.redis.reader_endpoint_address }

terraform apply builds the identical stack; terraform destroy removes it. To run cluster mode enabled instead, drop num_cache_clusters and set num_node_groups (shards) and replicas_per_node_group.

Common mistakes & troubleshooting

The cache playbook. Symptom → root cause → the exact command or console path to confirm → the fix.

# Symptom Root cause Confirm (exact command / path) Fix
1 redis-cli hangs / times out from your laptop ElastiCache is in-VPC only — no public endpoint nslookup $PRIMARY returns a private 10.x/172.x IP; you’re outside the VPC Connect from a host in the VPC (EC2/Lambda) or via SSM/VPN — never expose it
2 Connection refused / timeout from the app Security group doesn’t allow the app on 6379 aws ec2 describe-security-groups --group-ids $REDIS_SG shows no 6379 ingress authorize-security-group-ingress --port 6379 --source-group <app-sg>
3 NOAUTH Authentication required An AUTH token is set; the client didn’t send it describe-replication-groups --query '..AuthTokenEnabled' = true Pass the token (redis-cli -a / client password=); over TLS
4 WRONGPASS invalid username-password Wrong/rotated AUTH token, or RBAC user mismatch Compare the token; check the RBAC user’s access string Use the current token; for RBAC pass the right user/password
5 Handshake fails / Error: Connection reset on connect In-transit encryption is on but the client isn’t using TLS describe-replication-groups --query '..TransitEncryptionEnabled' = true Connect with --tls (redis-cli) / ssl=True (client)
6 Low CacheHitRate (lots of misses) TTL too short, poor key design, or a cold cache CloudWatch CacheHitRate/CacheMisses; INFO stats keyspace ratio Lengthen/jitter TTLs, fix key naming, pre-warm hot keys
7 Rising Evictions, DatabaseMemoryUsagePercentage ~100% Node too small or wrong eviction policy CloudWatch Evictions + memory %; INFO memory Bigger cache.r node, add shards, or set allkeys-lru
8 OOM command not allowed when used memory > 'maxmemory' noeviction, or volatile-lru with no evictable (TTL) keys CONFIG GET maxmemory-policy; check keys have TTLs Set allkeys-lru; add TTLs; scale the node
9 Wave of write errors for ~30–60s A failover repointed the primary; client didn’t reconnect ElastiCache Failover event; describe-events Connect by endpoint DNS, add reconnect/retry, handle READONLY
10 READONLY You can't write against a read only replica App is writing to the reader endpoint (or a replica) Check which endpoint the writer uses Send writes to the primary endpoint; reads to the reader
11 Periodic DB CPU spike synced to a TTL Thundering herd — hot keys expire together DB CPU/connections spike on the TTL boundary Add TTL jitter; use a SET NX rebuild lock; serve stale on miss
12 Latency spikes for all clients at once A single O(N) command blocked the single thread SLOWLOG GET; look for KEYS, big HGETALL/SMEMBERS/DEL Replace KEYS with SCAN; split big keys; avoid O(N) on the hot path
13 EngineCPUUtilization pinned at ~100% (host CPU looks fine) The Redis thread is saturated (host has spare cores) Compare EngineCPUUtilization vs CPUUtilization Scale up, shard (cluster mode), or cut expensive commands
14 SwapUsage > 0 and climbing Memory pressure — the node is swapping CloudWatch SwapUsage/FreeableMemory Resize to more memory; never let a cache swap
15 CROSSSLOT Keys in request don't hash to the same slot Multi-key op across slots in cluster mode The command touches keys in different slots Use a hash tag {...} to co-locate, or split the op
16 MOVED/ASK errors from the app Client isn’t cluster-aware in cluster mode You’re on a config endpoint with a non-cluster client Use a cluster-mode client (redis-cli -c); follow redirects
17 max number of clients reached Connection leak / no pooling exhausts maxclients CloudWatch CurrConnections near the cap Pool and reuse connections; fix leaks; bigger node raises the cap

Redis error / status reference

Error string Meaning Likely cause Fix
NOAUTH Authentication required No/invalid auth on a protected cache Missing AUTH token Send the token over TLS
WRONGPASS invalid username-password pair Bad credentials Wrong/rotated token or RBAC user Use current credentials
OOM command not allowed … Write rejected, memory full noeviction/no evictable keys allkeys-lru; add TTLs; scale
READONLY You can't write … Wrote to a replica Reader endpoint / post-failover Write to the primary endpoint
MOVED <slot> <host> Slot lives elsewhere (permanent) Cluster redirect Cluster-aware client updates its map
ASK <slot> <host> Slot migrating (temporary) Resharding in progress Client retries on the new shard
CROSSSLOT Keys … Multi-key op spans slots Keys in different slots Hash tag {...} or split
CLUSTERDOWN Hash slot not served Cluster not fully covered A shard/slots unavailable Wait for recovery; check shard health
LOADING Redis is loading the dataset Node still loading into memory Just started / restored Retry after it finishes loading
max number of clients reached Hit maxclients Too many/leaked connections Pool connections; scale the node

ElastiCache node/group states you’ll see

State Meaning Usable?
creating Being provisioned No
available Healthy and serving Yes
modifying Applying a change Usually yes
snapshotting Taking a backup (brief memory fork) Yes (watch memory)
rebooting cache cluster nodes Node restart (e.g. static param) Briefly no
deleting Being torn down No
create-failed Provisioning failed No
restore-failed Snapshot restore failed No

The nastiest three, in prose

The eviction/OOM trap. ElastiCache defaults maxmemory-policy to volatile-lru, which only evicts keys that have a TTL. Teams cache some keys with a TTL and others without, the node fills, volatile-lru finds nothing evictable among the no-TTL keys, and every write starts failing with OOM command not allowed when used memory > 'maxmemory' — an outage that looks like Redis “breaking” but is pure configuration. Confirm with redis-cli … CONFIG GET maxmemory-policy and INFO memory. Fix: set allkeys-lru so any key is evictable, ensure your cache keys carry TTLs, and size the node so the hot set fits with reserved-memory-percent headroom.

The thundering herd on expiry. A flat TTL means a batch of hot keys expires at the same instant; the next moment, every request for those keys misses simultaneously and stampedes the database — the exact load spike the cache was meant to absorb, now arriving on a timer. You’ll see a database CPU/connection spike synchronized to the TTL boundary. The fix is layered: jitter every TTL (base + random(0, spread)) so expiries spread out, guard genuinely hot keys with a SET key val NX PX rebuild lock so one request repopulates while others serve stale or briefly wait, and for the hottest keys refresh them before they hard-expire.

The single-threaded self-inflicted stall. Redis executes commands on one thread, so a single O(N) operation — the classic KEYS * in a debugging session, a SMEMBERS on a million-member set, HGETALL on a giant hash, or a huge DEL — blocks every other client for its whole duration, and everyone everywhere sees a latency spike. SLOWLOG GET will show the culprit; EngineCPUUtilization spikes while host CPUUtilization looks calm (other cores idle). The fixes are permanent: never run KEYS in production (use SCAN, which is incremental), avoid unbounded big keys (shard them, or use scan-style iteration), and keep O(N) commands off the hot path entirely.

Best practices

Security notes

ElastiCache security is layered, and the cache holds copies of your hottest, often most sensitive data:

Cost & sizing

ElastiCache billing is mostly node-hours, but there are more drivers than one number. Know them so nothing surprises you:

Cost driver How it’s billed Notes / how to control
Node hours Per node, per hour, by node type The biggest lever; a 3-node Multi-AZ group bills a single node
Replicas Each replica is a full billed node More replicas = more read capacity and more cost
Multi-AZ Replicas in other AZs (node cost) + cross-AZ data transfer HA has a real, recurring price
Node family cache.t (burstable) < cache.m < cache.r (memory) Pay for memory with cache.r; Graviton (g) is cheaper
Serverless Data stored (GB-hour) + ECPUs per request No idle node cost; can beat node-based for bursty load
Backup storage Free up to the cluster size; overage per GB-month Long retention / many snapshots add up
Data transfer Cross-AZ replication and client traffic Keep app and primary in the same AZ where sensible
Reserved nodes 1- or 3-year commit Up to ~55% off node hours for steady workloads

Rough sizing to anchor expectations (on-demand, us-east-1, order-of-magnitude — confirm in the calculator; ₹ at ~83/USD):

Workload Node type Topology Rough USD/mo Rough INR/mo
Free-tier learning cache.t3.micro Single node ~$0 (12 mo) ~₹0
Small dev cache cache.t4g.micro Single node ~$12 ~₹1,000
Small HA prod cache cache.t4g.medium 1 primary + 1 replica (Multi-AZ) ~$70 ~₹5,800
Steady prod cache cache.r6g.large 1 primary + 2 replicas ~$300 ~₹25,000
Large sharded cache cache.r6g.xlarge × 3 shards 3 primaries + 3 replicas ~$1,800 ~₹1,50,000
Bursty / unpredictable Serverless Auto Usage-based Usage-based

Free tier (first 12 months): 750 hours/month of a cache.t2.micro or cache.t3.micro node. Stay on a single micro node and the lab is effectively free — but a Multi-AZ three-node group is not free-tier-covered, so tear it down. The biggest real-world saving is second-order: a cache that offloads 90% of reads often lets you remove read replicas from the database, which usually more than pays for the cache itself.

Interview & exam questions

1. What is cache-aside (lazy loading), and what are its two main downsides? The app checks the cache, and on a miss reads the database and populates the cache with a TTL. Downsides: a cold cache (empty until warmed, so initial reads miss) and staleness (a cached value can be up to its TTL out of date after a DB change unless you invalidate on write). (DVA-C02, SAA-C03)

2. Cache-aside vs write-through — when would you choose write-through? Write-through writes cache and DB together on every write, so a written key is never stale and is warm on the next read — choose it for data written once and read many times where freshness matters, usually combined with cache-aside. The cost is higher write latency and caching data that may never be read. (DVA-C02)

3. Why is ElastiCache Redis single-threaded a problem, and what triggers it? Commands execute on one thread, so a single O(N) command (KEYS *, SMEMBERS/HGETALL on a huge key, a big DEL) blocks all other clients and spikes latency for everyone. Watch EngineCPUUtilization, use SCAN not KEYS, and avoid big keys. (SOA-C02)

4. What’s the difference between cluster mode disabled and enabled? Disabled is a single shard (one primary + up to 5 replicas), scaled vertically, with a primary and reader endpoint and any client. Enabled shards data across up to 500 node groups over 16,384 hash slots, scaled horizontally, needs a cluster-aware client (MOVED/ASK), limits multi-key ops to one slot, and uses a configuration endpoint. (SAA-C03)

5. A hot key expires and the database gets slammed. Name it and fix it. A cache stampede / thundering herd. Fix with TTL jitter so keys don’t expire together, a SET NX rebuild lock so one request repopulates while others serve stale, and early/background refresh of the hottest keys. (DVA-C02, SAA-C03)

6. Why does a Redis write fail with OOM command not allowed, and how do you fix it? The node hit maxmemory and the eviction policy can’t free anything — either noeviction, or volatile-lru with no TTL-bearing keys to evict. Fix: set allkeys-lru, ensure keys have TTLs, and/or scale the node. (SOA-C02)

7. Which endpoint do you send writes to, and what happens on failover? Writes go to the primary endpoint (a DNS name). On a failover ElastiCache promotes a replica and repoints that DNS to the new primary automatically (~30–60s); clients that connect by name and reconnect recover, while ones caching the IP break. (SAA-C03, SOA-C02)

8. Redis OSS vs Memcached — one deciding factor each way. Redis for data structures, replication, Multi-AZ failover, cluster and persistence; Memcached for a simple, multithreaded, shardable blob cache with no replication when you don’t need any of that. (SAA-C03)

9. How do you secure an ElastiCache Redis cluster? Private subnets, an SG allowing 6379 only from the app SG, TLS in transit, KMS at rest, and RBAC users with least-privilege ACLs (or an AUTH token over TLS). It’s in-VPC only — there’s no public endpoint. (SCS, SAA-C03)

10. When is ElastiCache Serverless the better choice than a node-based cluster? When traffic is spiky or unpredictable, or you want zero capacity management — it scales automatically and bills on data stored + ECPUs. A steady, well-understood high-throughput workload can be cheaper on a precisely-sized cache.r node. (SAA-C03)

11. What data structure powers a real-time leaderboard, and why? A sorted set (ZSET): ZINCRBY to update a score and ZREVRANGE 0 9 for the top ten, both O(log N) — far cheaper than an ORDER BY … LIMIT over a hot relational table. (DVA-C02)

12. Which CloudWatch metric is the real saturation signal for Redis, and why not CPUUtilization? EngineCPUUtilization — it measures the single Redis command thread. CPUUtilization averages all host cores, so a saturated Redis thread can hide behind idle cores and look fine. (SOA-C02)

Quick check

  1. True/false: ElastiCache Redis has a public endpoint you can reach from the internet if you open the security group.
  2. What one parameter value turns a cache that returns OOM into one that evicts cold keys, and what should it be?
  3. A flat 60-second TTL on all your hot keys causes a database CPU spike every minute. What is it and what’s the fix?
  4. Which endpoint do writes go to, and what breaks if the client caches its IP?
  5. Name the metric you should alert on for Redis CPU saturation, and the one that misleads.

Answers

  1. False. ElastiCache is in-VPC only — there is no public endpoint. You reach it from within the VPC (or peered/VPN); the SG governs which VPC workloads can connect on 6379.
  2. maxmemory-policy, set to allkeys-lru (or allkeys-lfu). The ElastiCache default volatile-lru only evicts keys with a TTL, so no-TTL keys cause OOM.
  3. A thundering herd / cache stampede — the keys all expire at the same instant and every request misses at once. Fix with TTL jitter and a SET NX rebuild lock (and early refresh for the hottest keys).
  4. Writes go to the primary endpoint (DNS). If the client caches the resolved IP, a failover repoints the DNS to a new primary and the client keeps hitting the old node — errors until it reconnects by name.
  5. Alert on EngineCPUUtilization (the single Redis thread); CPUUtilization (all host cores averaged) misleads because a saturated thread hides behind idle cores.

Glossary

Term Definition
ElastiCache AWS-managed in-memory cache service running Redis OSS, Valkey or Memcached in your VPC.
Redis OSS / Valkey Rich single-threaded in-memory data-structure stores; Valkey is the open-source Redis fork AWS prices lower.
Memcached A simple, multithreaded, volatile blob cache with no replication, failover or data structures.
Cache-aside (lazy loading) The app checks the cache and, on a miss, reads the DB and populates the cache with a TTL.
Write-through The app writes cache and DB together on every write, keeping written keys warm and fresh.
Write-behind The app writes the cache, which flushes to the DB asynchronously — lowest write latency, risk of loss.
Hit rate The fraction of reads served from cache; the primary measure of a cache’s value.
TTL Time-to-live; a per-key expiry that bounds staleness and lets the cache self-heal toward the source.
Thundering herd / stampede Many requests missing a hot key at once (often on synchronized expiry) and overloading the DB.
Replication group The ElastiCache Redis unit: one or more shards, each a primary plus 0–5 read replicas.
Shard (node group) A primary and its replicas; cluster mode enabled partitions data across many shards.
Cluster mode Disabled = single shard; enabled = data sharded across up to 500 node groups over 16,384 hash slots.
Hash slot One of 16,384 buckets (CRC16(key) mod 16384) that place a key on a shard in cluster mode.
Multi-AZ / automatic failover Replicas in other AZs with auto-promotion of a replica if the primary fails (~30–60s).
Primary / reader / configuration endpoint DNS names for writes, load-balanced reads, and cluster discovery respectively.
Eviction policy (maxmemory-policy) What Redis drops at maxmemory; allkeys-lru for a cache, noeviction returns OOM.
AUTH token / RBAC A shared password vs per-user ACL access strings scoping commands and key patterns.
ECPU ElastiCache Processing Unit — the per-request compute unit billed on Serverless.

Next steps

AWSElastiCacheRedisValkeyCachingCache-asideMulti-AZDatabases
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading