ElastiCache (Redis & Memcached)
Amazon ElastiCache is a managed in-memory cache service. An in-memory cache stores data in RAM (very fast memory) instead of on disk, so reads return in microseconds instead of the milliseconds a database takes. You point your application at ElastiCache to remember expensive query results, session data, or hot objects, which cuts latency for your users and takes load off your main database. ElastiCache runs two engines: Redis (now branded “Valkey/Redis OSS” in newer AWS consoles) and Memcached. AWS handles the servers, patching, failover, and backups for you.
Why cache at all
Every read your database serves costs CPU, disk I/O, and connection slots. If thousands of users keep asking for the same product page or the same user profile, you are making your database do the same work over and over. A cache stores the answer the first time and serves it from memory afterward.
When to use this: read-heavy workloads where the same data is requested repeatedly (product catalogs, leaderboards, session tokens, API rate-limit counters, computed feeds). When NOT to use it: write-heavy data that changes on every request, or data where serving a slightly stale value is unacceptable and you cannot invalidate quickly. Caching adds a second system you must keep consistent, so only cache where the speed-up is worth that complexity.
Redis vs Memcached — which engine
Both store key-value pairs in memory, but they are very different in capability.
| Feature | Redis (Valkey/Redis OSS) | Memcached |
|---|---|---|
| Data types | Strings, lists, sets, sorted sets, hashes, bitmaps, streams | Strings only |
| Replication & failover | Yes (Multi-AZ with automatic failover) | No |
| Persistence (snapshots) | Yes | No |
| Pub/sub messaging | Yes | No |
| Multi-threaded | Limited (single-threaded core) | Yes (scales on big nodes) |
| Best for | Almost everything new | Simple, huge, transient caches |
Gotcha: Pick Redis for almost every new project. Memcached has no replication, no failover, and no persistence, so a node restart loses all data and there is no standby to take over. Choose Memcached only when you want a dead-simple cache that scales horizontally across many cores and you genuinely do not need any of Redis’s features.
How caching fits your app
ElastiCache nodes live inside your VPC (Virtual Private Cloud, your private network in AWS). They are not exposed to the internet. Your application servers connect to the cluster’s endpoint (a DNS hostname) over a port (6379 for Redis, 11211 for Memcached). You control access with a security group (a virtual firewall) that only allows your app servers in.
Create a Redis cluster
Console steps
- Open the ElastiCache console and choose Redis OSS caches (or Valkey caches), then Create.
- Choose Design your own cache and the Cluster mode setting. Leave cluster mode disabled for a simple primary-plus-replicas setup; enable it only when one node cannot hold your data and you need sharding.
- Name the cluster (for example
my-app-cache) and pick a node type such ascache.t4g.microfor dev orcache.r7g.largefor production. - Set Number of replicas to 2 for high availability, and turn on Multi-AZ so a replica is automatically promoted if the primary fails.
- Under Subnet group, pick the subnets in your VPC (use private subnets). Under Security, attach a security group like
sg-0a1b2c3d. - Enable encryption in transit and encryption at rest, set an automatic backup retention (for example 7 days), and choose Create.
CLI equivalent
aws elasticache create-replication-group \
--replication-group-id my-app-cache \
--replication-group-description "App cache" \
--engine redis \
--cache-node-type cache.t4g.micro \
--num-node-groups 1 \
--replicas-per-node-group 2 \
--automatic-failover-enabled \
--multi-az-enabled \
--cache-subnet-group-name my-cache-subnets \
--security-group-ids sg-0a1b2c3d \
--transit-encryption-enabled \
--at-rest-encryption-enabled
Output:
{
"ReplicationGroup": {
"ReplicationGroupId": "my-app-cache",
"Status": "creating",
"AutomaticFailover": "enabling",
"MultiAZ": "enabled",
"ClusterEnabled": false,
"TransitEncryptionEnabled": true,
"AtRestEncryptionEnabled": true
}
}
Once the status reaches available, fetch the primary endpoint your app will connect to:
aws elasticache describe-replication-groups \
--replication-group-id my-app-cache \
--query "ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint"
Output:
{
"Address": "my-app-cache.abc123.ng.0001.use1.cache.amazonaws.com",
"Port": 6379
}
Cost note: A
cache.t4g.micronode runs about $0.016/hour (roughly $12/month) in us-east-1, fine for dev. A productioncache.r7g.largewith two replicas is three nodes at roughly $0.21/hour each, around $460/month total. You pay per node-hour plus a little for backup storage and data transfer across Availability Zones.
The cache-aside pattern
Cache-aside (also called “lazy loading”) is the most common way to use ElastiCache. Your application, not the cache, owns the logic: check the cache first, fall back to the database on a miss, then write the value into the cache for next time.
import redis, json
cache = redis.Redis(
host="my-app-cache.abc123.ng.0001.use1.cache.amazonaws.com",
port=6379, ssl=True, socket_timeout=0.2,
)
def get_user(user_id, db):
key = f"user:{user_id}"
try:
cached = cache.get(key)
if cached:
return json.loads(cached) # cache hit
except redis.RedisError:
pass # cache down -> fall through to DB
user = db.query_user(user_id) # cache miss -> read DB
try:
cache.set(key, json.dumps(user), ex=300) # store for 5 minutes
except redis.RedisError:
pass # don't fail the request if write fails
return user
Two things make this safe. First, every value has a TTL (time to live, here ex=300 seconds), so stale data eventually expires even if you forget to invalidate it. Second, every cache call is wrapped so that if the cache is unreachable, the app degrades to the database instead of throwing an error.
Invalidation
When the underlying data changes, you must remove or overwrite the cached copy, or readers will keep seeing the old value until the TTL expires.
def update_user(user_id, fields, db):
db.update_user(user_id, fields)
try:
cache.delete(f"user:{user_id}") # next read repopulates from DB
except redis.RedisError:
pass
Warning: Invalidation is the hard part of caching. Deleting the key on every write (as above) is simpler and safer than trying to update the cached value in place, because an in-place update can race with a concurrent read and leave the wrong value cached.
Best practices
- Choose Redis over Memcached for any new workload unless you specifically need only a simple multi-threaded cache.
- Always wrap cache calls in error handling so a cache outage degrades to the database rather than failing the whole app.
- Set a sensible TTL on every key so stale data self-heals even if invalidation is missed.
- Enable Multi-AZ with at least one replica and automatic failover for production Redis clusters.
- Turn on encryption in transit and at rest, and lock the security group to only your application servers.
- Watch the
Evictions,CacheMisses, andDatabaseMemoryUsagePercentageCloudWatch metrics; rising evictions mean the node is too small. - Never store the only copy of data in Memcached, and treat even Redis as a cache, not a primary store, unless persistence is fully configured.