Filter concepts by levelShowing all levels.

AWS · Section 25

ElastiCache / Managed Caching

Level
advanced
Read
40 min
Concepts
6

ElastiCache runs three engines — Memcached (simple, multithreaded, no replication) and Valkey/Redis OSS (data structures, pub/sub, replication with automatic failover) — and the choice between them follows directly from which capabilities a workload actually uses. Cache-aside and write-through are the two ways to keep a cache populated, each with its own staleness and gap trade-offs that TTL and invalidation manage; a hot key's expiry can still trigger a stampede of concurrent misses, which a best-effort distributed lock reduces but does not perfectly prevent. Session storage, rate limiting, counters, and pub/sub all build directly on TTL-bearing keys and atomic operations. Multi-AZ failover resumes writes in typically a few seconds through the same endpoint, but asynchronous replication means it is not a durability guarantee — which is exactly why a cache must always be treated as an expendable, rebuildable copy, never the only place data lives, unless durability is explicitly and deliberately configured for that purpose.

What is true here

  1. Memcached is simple and multithreaded with no replication or pub/sub; Valkey/Redis OSS add data structures, pub/sub, and replication with automatic failover — pick based on which the workload actually needs.
  2. Cache-aside fills on a read miss and can go stale; write-through fills on every write and is never stale but leaves gaps on a fresh node — TTL bounds staleness for either.
  3. A cache stampede is many concurrent misses on one hot key hitting the database at once; a SET NX PX distributed lock reduces this but is a best-effort mutual-exclusion primitive, not a hard guarantee.
  4. Session storage, rate limiting, and counters all build on TTL-bearing keys and atomic INCR/DECR; pub/sub is unrelated to the key space and delivers only to current subscribers.
  5. A cache is never the system of record — a node replacement, eviction, or failover can destroy anything stored only there, unless Valkey durability is deliberately enabled for that exact use case.

What you will be able to do

  • Choose Memcached or Valkey/Redis OSS based on whether the workload needs data structures, pub/sub, or replication
  • Decide between cache-aside and write-through (or both together) based on staleness tolerance and fresh-node behavior
  • Recognize a cache-stampede risk and apply a distributed lock or jittered TTL, understanding its best-effort limits
  • Build session storage, rate limiting, counters, and pub/sub features on the correct TTL and atomic-operation primitives
  • Explain what Multi-AZ failover does and does not guarantee, and why a cache must never be the only copy of data
From engine choice to the system-of-record boundary
determinesavailablehardenedagainstapplied toboth assumeboth assumeeven withfailover

Redis/Valkey vs Memcached

Cache-aside, write-through, TTL

Stampede prevention, locks

Sessions, rate limits, pub/sub

Multi-AZ failover

Never the system of record

  • Redis/Valkey vs Memcached
    • leads to Cache-aside, write-through, TTL (determines available)
  • Cache-aside, write-through, TTL
    • leads to Stampede prevention, locks (hardened against)
    • leads to Sessions, rate limits, pub/sub (applied to)
  • Stampede prevention, locks
    • leads to Multi-AZ failover (both assume)
  • Sessions, rate limits, pub/sub
    • leads to Multi-AZ failover (both assume)
  • Multi-AZ failover
    • leads to Never the system of record (even with failover)
  • Never the system of record

ElastiCache / Managed Caching

Engine choice (Redis/Valkey vs Memcached), cache-population strategies, stampede prevention and locking caveats, application patterns built on the cache, availability and failover, and the system-of-record boundary.

Redis vs Memcached Use Cases

coreintermediate

ElastiCache runs three engines: Valkey, Redis OSS, and Memcached. Memcached is simpler and multithreaded, good for pure key-value object caching that must scale out across many nodes. Valkey and Redis OSS add data structures (sorted sets, hashes, lists), pub/sub, and replication with automatic failover, at the cost of being single-threaded per node.

Think of it as

Memcached is a fast, disposable whiteboard — write anything, erase it anytime, and if the board breaks you just get a new one with nothing on it. Redis OSS/Valkey is a shared notebook with structure — sorted pages, cross-references, and a backup copy (replica) that can take over if the original notebook is destroyed.

What we're doing: Pick the right engine for two different caching needs on the same application.

engine-choice.txttext
1. Cache rendered HTML fragments, purely by key, across 12 large nodes
   → Memcached: simple values, multithreaded, scales out easily

2. Real-time gaming leaderboard with live rank updates and a
   "player joined" notification stream
   → Valkey or Redis OSS: sorted sets (ZADD/ZREVRANGEBYSCORE) for
     ranking, pub/sub for the notification stream
1
Rendered HTML fragments are opaque blobs looked up by key — Memcached's simple string values and multithreading are the better fit, and there is no data structure or failover requirement to justify Redis/Valkey.
4
A leaderboard needs a data structure the application would otherwise reimplement in every server, and the notification stream needs pub/sub — both are native to Valkey/Redis OSS and absent from Memcached.

Why this works: The two workloads look similar on the surface ("cache some data"), but one needs nothing beyond a key-value store while the other needs server-side data structures and messaging — the engine choice follows directly from which capabilities the workload actually uses, not from which engine is "better" in general.

Defaulting to Redis/Valkey for a workload that only needs plain key-value caching

Wrong

text
# "Redis is the more popular/full-featured one, use it everywhere
# regardless of whether the workload needs data structures or failover."

Better

text
# Pick Memcached when the workload is simple object caching that needs
# multithreading and easy horizontal scale-out with no failover requirement

What you see: A large single-node Memcached workload migrated to Redis/Valkey loses the multithreading benefit on that node and needs more, smaller nodes to use the same number of cores — with no data-structure or pub/sub benefit ever used.

Why: Redis/Valkey's single-threaded-per-node model and Memcached's multithreaded model create genuinely different scaling shapes — using Redis/Valkey by default for a workload that never touches its data structures, replication, or pub/sub pays that scaling cost for a capability set the workload never needed.

Memcached vs Valkey/Redis OSS

Memcached

  • +Simple string/object values only
  • +Multithreaded — scales cores within one node
  • +No replication, no automatic failover
  • +No pub/sub

Valkey / Redis OSS

  • Sorted sets, hashes, lists, geospatial data types
  • Single-threaded per node — scale by adding shards
  • Replication with automatic failover
  • Pub/sub messaging built in
  • Memcached
    • Simple string/object values only
    • Multithreaded — scales cores within one node
    • No replication, no automatic failover
    • No pub/sub
  • Valkey / Redis OSS
    • Sorted sets, hashes, lists, geospatial data types
    • Single-threaded per node — scale by adding shards
    • Replication with automatic failover
    • Pub/sub messaging built in

Memcached vs Valkey/Redis OSS — deciding factors

Memcached vs Valkey/Redis OSS — deciding factors
PropertyMemcachedValkey / Redis OSS
Data typesSimple strings/objects onlyStrings, sorted sets, hashes, lists, bitmaps, geospatial
Multithreaded per nodeYesNo
Replication / automatic failoverNoYes
Pub/SubNoYes
Data partitioningClient-side, built inServer-side in cluster mode, with online resharding

Together

text
Session cache, no cross-node structure needed → Memcached (simple, multithreaded)
Leaderboard + pub/sub notifications, need failover → Valkey or Redis OSS

Remember: Memcached: simple values, multithreaded, no replication/failover, no pub/sub — pick it for pure object caching that scales out. Valkey/Redis OSS: data structures, pub/sub, replication with automatic failover — pick it when the workload needs any of those.

See also: cache aside write through and ttl · cache availability and failover

Cache-Aside, Write-Through, TTL, and Invalidation

coreintermediate

Cache-aside (lazy loading) reads the cache first, and on a miss queries the database and writes the result into the cache — only requested data is ever cached, but data can go stale until the next miss. Write-through updates the cache every time the database is updated, so the cache is never stale, but costs a write to the cache on every write and can leave the cache empty for data that was never read since a node was replaced. TTL puts a time limit on both, expiring keys so reads eventually re-fetch fresh data.

Think of it as

Cache-aside is a "check first, ask if missing" habit: look on your desk, and only go to the file room if it is not there — the desk can hold outdated paper if the file room changed it since. Write-through is updating your desk copy the moment you update the file room original, so the desk is never wrong, but every update is now two updates instead of one.

What we're doing: See why a cache-aside read on a cold cache costs three round trips, and how a TTL bounds staleness afterward.

cache-aside-ttl.txttext
get_customer(id):
    record = cache.get(id)
    if record is None:                 # cache miss
        record = db.query(id)           # trip 2: database
        cache.set(id, record, ttl=300)  # trip 3: write back, expires in 5 min
    return record                       # trip 1 was the cache.get above
1
Every read goes through the cache first — this function is called exactly the same way whether the data is cached or not.
5
The 300-second TTL means this key self-expires — the next read after 5 minutes is guaranteed to be a miss and re-fetch current data, bounding how stale it can ever get.

Why this works: A cache miss on cache-aside is not free — it is strictly slower than a cache hit because it does the database query anyway, plus a cache write. TTL is what keeps a cache-aside cache from serving arbitrarily old data forever between misses.

Using cache-aside with no TTL and no invalidation on writes

Wrong

text
# cache.set(id, record)   # no ttl argument, and nothing deletes
# the key when the underlying row changes elsewhere

Better

text
# Always set a TTL as a staleness backstop, AND invalidate the key at
# the exact write that changes it: cache.delete(id); db.update(id, values)

What you see: A customer record updated by an admin tool or batch job keeps returning the old value to normal application reads indefinitely, because nothing ever expired or deleted the stale cache entry.

Why: A cache-aside cache is only as fresh as its next miss — with no TTL and no invalidation, a key populated once can outlive the data it was copied from for as long as the process keeps running, because nothing ever forces a re-fetch.

Cache-aside vs write-through

Cache-aside

  • +Cache filled only on a read miss
  • +Cache miss = 3 trips: cache, database, cache
  • +Node failure just means more misses, not an outage

Write-through

  • Cache updated on every database write
  • Cache is never stale
  • A fresh node has gaps until each key is rewritten
  • Cache-aside
    • Cache filled only on a read miss
    • Cache miss = 3 trips: cache, database, cache
    • Node failure just means more misses, not an outage
  • Write-through
    • Cache updated on every database write
    • Cache is never stale
    • A fresh node has gaps until each key is rewritten

Cache-aside vs write-through — trade-offs

Cache-aside vs write-through — trade-offs
PropertyCache-aside (lazy loading)Write-through
When cache is populatedOn a read missOn every database write
StalenessPossible until next miss or TTL expiryNever stale (updated with every write)
Extra costCache-miss penalty: 3 trips instead of 1Cache-write penalty on every database write
New/replaced node behaviorRepopulates itself from misses — app keeps workingMissing data until each key is written again
Wasted cache spaceLow — only requested data is cachedHigher — data that is never read is still cached

Together

text
save_customer(id, values):
    db.update(id, values)
    cache.set(id, values, ttl=300)   # write-through + TTL together

Remember: Cache-aside: fill on read miss, 3-trip miss penalty, survives node failure gracefully, can go stale. Write-through: fill on every write, never stale, leaves gaps on a fresh node, wastes space on unread data. TTL bounds staleness for either; invalidation on write removes it immediately instead of waiting.

See also: redis vs memcached use cases · cache stampede and distributed locks

Cache Stampede Prevention and Distributed Locks

coreadvanced

A cache stampede happens when a popular key expires and many concurrent requests all miss at once, sending every one of them to the database simultaneously. A distributed lock (SET key value NX PX ttl) lets only one process rebuild the key while others wait or serve stale data — but a naive single-key lock is not a hard correctness guarantee, only a best-effort one.

Think of it as

A cache stampede is one shop's entire lunchtime queue arriving at the exact moment the only cashier steps away — everyone converges on the till (the database) at once. A distributed lock is a "please wait, one person restocking" sign: mostly effective, but if the sign falls down at the wrong moment (a process pause or clock drift), two people can still walk in at once.

What we're doing: See the SET NX PX lock pattern used to let only one process rebuild an expired hot key.

stampede-lock.txttext
get_or_rebuild(key):
    value = cache.get(key)
    if value is not None:
        return value

    token = random_string()
    if cache.set("lock:" + key, token, nx=True, px=5000):   # acquired
        value = db.query(key)
        cache.set(key, value, ttl=300)
        release_lock("lock:" + key, token)
        return value
    else:                                                    # someone else holds it
        sleep(0.05)
        return get_or_rebuild(key)   # retry — usually finds the rebuilt key
2
A hot key's expiry means many concurrent callers reach this exact point at once — without a lock, every one of them would fall through to the database query below.
8
Only the first caller to win the NX SET rebuilds the key; every other concurrent caller gets a false return and retries shortly, usually finding the key already rebuilt by then.

Why this works: The lock turns "every concurrent miss queries the database" into "one miss queries the database, the rest wait a few milliseconds and re-read the cache" — this is the entire value of stampede prevention, and it is a database-load optimization, not a correctness mechanism.

Releasing the lock with a plain DEL instead of a value check

Wrong

text
# release_lock(lock_key):
#     cache.delete(lock_key)   # deletes whatever is there now

Better

text
# release_lock(lock_key, token):
#     # only delete if the value still matches this holder's token
#     if cache.get(lock_key) == token:
#         cache.delete(lock_key)

What you see: A slow holder finishes its rebuild after the lock already expired and a second process acquired it — the slow holder's unconditional DEL removes the second process's active lock, letting a third process acquire it too.

Why: A plain DEL has no way to know whether the key it is deleting is still "its" lock — Redis's own documentation flags exactly this race and recommends signing the lock with a unique token checked before deletion, precisely to avoid one client releasing a lock another client now holds.

No lock vs distributed lock on cache-miss rebuild

No lock

  • +Hot key expires
  • +All concurrent requests miss together
  • +Every one queries the database at once

With SET NX PX lock

  • Hot key expires
  • First request acquires the lock and rebuilds it
  • Others wait briefly or serve the stale value, then re-read
  • No lock
    • Hot key expires
    • All concurrent requests miss together
    • Every one queries the database at once
  • With SET NX PX lock
    • Hot key expires
    • First request acquires the lock and rebuilds it
    • Others wait briefly or serve the stale value, then re-read

Remember: A cache stampede is many concurrent misses on one hot key hitting the database at once. SET key value NX PX ttl lets one process rebuild while others wait — release with a value check, not a plain DEL. A single-key lock is best-effort, not a hard guarantee: process pauses or clock drift can still let two holders overlap.

See also: cache aside write through and ttl · session storage rate limiting and pubsub

Session Storage, Rate Limiting, Counters, and Pub/Sub

standardintermediate

Session storage, rate limiting, and counters all reuse a TTL-bearing key plus an atomic operation (SET EX for sessions, INCR for counts); pub/sub is different — it delivers a message to whichever clients are subscribed to a channel right now, with nothing stored in the key space and nothing kept for a client that subscribes later.

Think of it as

These four patterns all lean on the same two Redis/Valkey primitives: a TTL-bearing key (session storage, rate limiting) and an atomic counter or structure update (counters, sorted-set leaderboards) — pub/sub is the odd one out, using channels instead of keys at all.

text
SET session:<id> <data> EX 1800        # session storage, expires in 30 min
INCR ratelimit:<user>:<window>          # rate limiting / counters, atomic
EXPIRE ratelimit:<user>:<window> 60     # cap the counting window
PUBLISH channel message                 # pub/sub, no relation to any key

What we're doing: Implement a fixed-window rate limiter with an atomic counter and a TTL.

rate-limit.txttext
allow_request(user_id, limit=100, window_seconds=60):
    key = "ratelimit:" + user_id + ":" + current_minute()
    count = cache.incr(key)          # atomic increment, starts at 1 if new
    if count == 1:
        cache.expire(key, window_seconds)   # set TTL only on the first hit
    return count <= limit
2
Bucketing the key by the current minute creates a new counter for each fixed window automatically — no cleanup logic needed once the TTL below expires it.
3
INCR is atomic, so concurrent requests from the same user in the same window never race each other into an incorrect count — the increment and the read happen as one operation.

Why this works: Redis/Valkey's INCR is a single atomic operation on the server, which is what makes it safe for many concurrent requests to share one counter without a separate lock — the TTL on the same key makes the counter self-cleaning per window.

Remember: Session storage and rate limiting both lean on a TTL-bearing key (expiry = timeout / window reset). Counters and rate limits both lean on atomic INCR/DECR to avoid read-modify-write races. Pub/sub is separate from the key space entirely — it delivers to current subscribers only, with nothing persisted for latecomers.

See also: redis vs memcached use cases · cache stampede and distributed locks

Cache Availability and Failover Implications

coreadvanced

Multi-AZ on a Valkey/Redis OSS replication group promotes the replica with the least replication lag to primary when the primary fails, usually completing in just a few seconds, and re-points the same primary endpoint via DNS so the application does not need to change anything. Because replication is asynchronous, a small amount of data written just before the failure can be lost. Memcached has no replication or automatic failover at all.

Think of it as

A Multi-AZ Redis/Valkey replication group is an understudy who has been following the lead actor's every move — when the lead is suddenly unable to perform, the understudy steps in within seconds, wearing the same costume (the same endpoint) so the audience barely notices. But the understudy only knows the last line they heard, so anything said in the last moment before the switch is lost.

What we're doing: See what actually happens, step by step, when only the primary node fails in a Multi-AZ-enabled replication group.

multi-az-failover.txttext
1. Primary node fails (health check / connectivity loss)
2. Replica with the LEAST replication lag is promoted to primary
   -> writes can resume once promotion completes, typically a few seconds
3. ElastiCache propagates the promoted replica's DNS to the primary endpoint
   -> application keeps writing to the same endpoint, no code change
4. A replacement read replica is launched in the failed primary's AZ
5. The replacement syncs from the new primary
2
AWS documentation is explicit that the replica with the least replication lag is chosen, specifically to minimize how much data was not yet replicated when the primary failed.
3
The primary endpoint's DNS name is what gets re-pointed — this is why an application writing to the primary endpoint (not an individual node's address) needs no reconfiguration during failover.

Why this works: The whole design goal of Multi-AZ is to replace "recreate and reprovision a new primary from scratch" (slow, and requires an application endpoint change) with "promote an already-running, already-synced replica" (a few seconds, same endpoint) — understanding which replica gets chosen and why the endpoint does not change is what "failover implications" actually means in practice.

Connecting to individual node endpoints instead of the primary/reader endpoints

Wrong

text
# app.connect("redis12-001.xxxxxx.0001.usw2.cache.amazonaws.com")
# a specific node's own endpoint, not the primary endpoint

Better

text
# app.connect("redis12.xxxxxx.ng.0001.usw2.cache.amazonaws.com")
# the primary endpoint — DNS is re-pointed to the new primary on failover

What you see: After a failover completes and the old primary node is gone, the application keeps failing to connect, even though a new primary is up and serving traffic on a different node.

Why: Only the primary and reader endpoints are re-pointed by ElastiCache during failover — an individual node's own endpoint is tied to that specific node and does not follow the promotion, so connecting to it bypasses the entire mechanism Multi-AZ provides.

Failure without Multi-AZ vs with Multi-AZ

Without Multi-AZ

  • +Failed primary must be fully recreated and reprovisioned
  • +No promotion — this takes materially longer
  • +Writes are unavailable until the new primary is ready

With Multi-AZ

  • Least-lagging replica is promoted to primary
  • Typically just a few seconds to resume writes
  • Same primary endpoint, no application change needed
  • Without Multi-AZ
    • Failed primary must be fully recreated and reprovisioned
    • No promotion — this takes materially longer
    • Writes are unavailable until the new primary is ready
  • With Multi-AZ
    • Least-lagging replica is promoted to primary
    • Typically just a few seconds to resume writes
    • Same primary endpoint, no application change needed

Remember: Multi-AZ promotes the least-lagging replica on primary failure, typically in a few seconds, and re-points the same primary endpoint via DNS — no application change needed. Replication is asynchronous, so a small amount of just-written data can be lost. Memcached has no replication or failover at all.

See also: redis vs memcached use cases · cache is not the system of record

A Cache Is Not the System of Record

coreintermediate

A cache is meant to be treated as expendable: it should always be safe to lose it and rebuild from the real data store. By definition, cached data should be considered stale — using ElastiCache as the only place data lives means a node failure, eviction, or restart can destroy data with no other copy, unless durability is deliberately configured and chosen for that purpose.

Think of it as

A cache is a sticky note copied from a filing cabinet, not the filing cabinet itself — losing the sticky note is fine because the original is still filed away; losing the only filing cabinet is not fine at all. Treating the sticky note as if it were the only copy is the mistake, not the sticky note itself.

What we're doing: Contrast a design that treats the cache as expendable with one that accidentally makes it the only copy of data.

system-of-record.txttext
# Safe: cache is a derived, rebuildable copy
save_order(order_id, data):
    db.insert(order_id, data)          # the real, durable copy
    cache.set(order_id, data, ttl=600)  # a disposable accelerator

# Unsafe: cache is the only copy
record_cart_item(session_id, item):
    cache.append(session_id, item)     # nothing else stores this
1
The database write happens first and unconditionally — losing the cache entry afterward loses nothing, because the order still exists in the real system of record.
5
Nothing durable ever receives this write — if the node holding this key is replaced, restarted, or evicts the key under memory pressure, the cart item is gone permanently with no way to recover it.

Why this works: The difference is not about which API calls were used — it is about whether a second, durable copy of the data exists anywhere else. The safe version can lose the cache at any moment with zero data loss; the unsafe version cannot.

Storing shopping-cart or in-progress-workflow state only in ElastiCache

Wrong

text
# cache.append("cart:" + session_id, item)
# the cart only exists in the cache — nothing else records it

Better

text
# db.upsert_cart_item(session_id, item)   # durable copy
# cache.set("cart:" + session_id, cart, ttl=1800)  # accelerator only

What you see: A customer's shopping cart is silently emptied after a node failover, a planned maintenance replacement, or the key simply being evicted under memory pressure — with no error, because nothing signals that the "only copy" was lost.

Why: A cache node can be replaced, restarted, or evict keys for memory pressure at any time as part of normal, expected operation — treating any of that as an edge case rather than the default assumption is exactly the mistake this rule exists to prevent.

Safe: a rebuildable copy vs unsafe: the only copy

Safe design

  • +db.insert() writes the durable copy first
  • +cache.set() is a disposable accelerator
  • +Losing the cache loses nothing

Unsafe design

  • cache.append() — nothing else stores this
  • A node failure or eviction destroys it permanently
  • No error signals the loss
  • Safe design
    • db.insert() writes the durable copy first
    • cache.set() is a disposable accelerator
    • Losing the cache loses nothing
  • Unsafe design
    • cache.append() — nothing else stores this
    • A node failure or eviction destroys it permanently
    • No error signals the loss

Remember: A cache is expendable by design — it must always be safe to lose and rebuild from a real data store. Treat cached data as stale by definition. The one sanctioned exception is Valkey durability, deliberately enabled with synchronous writes for a genuine system-of-record use case — not the default behavior of any ElastiCache cluster.

See also: cache availability and failover · cache aside write through and ttl

Advertisement