This website uses cookies

Read our Privacy policy and Terms of use for more information.

What it actually is

Amazon ElastiCache runs an in-memory key-value engine for you and hands back an endpoint. The engines are Valkey, Redis OSS, and Memcached. It comes in two shapes: serverless caches, where you name a cache and AWS decides the topology, and node-based clusters, where you pick node types, node counts, and Availability Zones yourself.

The mental model the docs bury: ElastiCache does not make your application faster. A cache is a bet that reading a stored answer is cheaper than recomputing it, and that the stored answer is still good enough. AWS operates the memory, the replication, the patching, and the failover. You operate the bet. The hard part of a cache was never running it, it was deciding when an entry stops being true. That is the invalidation problem, and no managed service takes it off your plate.

If you remember one thing: ElastiCache sells you a fast place to put copies. Every copy is a lie waiting to age. The article below builds a cache, then measures how often the lie is told, which is the only number that tells you whether the cache earns its bill.

The model

There are two deployment options, and the choice shapes everything else. According to the deployment options page, serverless supports Valkey 7.2 and later, Redis OSS 7.1, and Memcached 1.6.22 and later, while node-based clusters support Valkey 7.2 and later, Redis OSS 4.0 to 7.1, and Memcached 1.4 and later.

Serverless runs only in cluster mode enabled. Your client connects to one endpoint, data is stored redundantly across three Availability Zones, and the documented availability SLA is 99.99%. You get a small set of knobs: subnets, automatic backups on or off, and maximum usage limits. Encryption at rest and in transit are always on, so your client must speak TLS. The serverless cache scales by tracking CPU, memory, and network use per cache, adding a shard and moving data to it when resources run short, with no downtime.

Node-based clusters give you parameter groups, node-type choice, cluster mode on or off, scheduled and metric-based Auto Scaling, and a few features serverless does not have: Global Data Store, data tiering on r6gd nodes, and durability through a Multi-AZ transactional log on Valkey 9.0 and later. You pay for nodes by the hour whether or not anyone is reading.

Two details about the data model matter early. First, because serverless is always cluster mode, keys hash into slots, and a multi-key command, transaction, or Lua script must touch keys in one slot or you get a CROSSSLOT error. Second, a slot is the unit of scaling and throttling: the documented limit is 30K ECPUs per second per slot, or 90K with Read from Replica on READONLY connections. One hot key can throttle itself no matter how large the cache is.

The currency of serverless compute is the ECPU, an ElastiCache Processing Unit. The pricing page says a simple read or write consumes one ECPU per KB transferred. Small commands cost one ECPU. Big values and heavy commands cost more.

Valkey 9.0 is the current engine line on ElastiCache. The May 2026 announcement lists full-text and hybrid search, up to 40% higher pipelined throughput, hash field expiration, and multi-database support in cluster mode, at no additional cost on both node-based and serverless.

When to use it, when not to

A cache earns its place when a read is expensive or frequent, and a slightly stale answer is acceptable. Here is how ElastiCache compares against the services people confuse it with.

Service

What it is

Pick it when

Skip it when

ElastiCache

In-memory cache and data structures, managed

You need sub-millisecond reads, shared state across app servers, rate limits, leaderboards, sessions

The cache would be your only copy of data you cannot lose

MemoryDB

Valkey/Redis-compatible database with a Multi-AZ transaction log

The in-memory store is the system of record

It is purely a disposable cache; you pay for durability you do not use

DynamoDB (with DAX)

Key-value database with an item cache in front

You already live in DynamoDB and want microsecond reads of items

Your hot data lives in Postgres or an API

A bigger database

Scale the source instead

The query is slow because it is unindexed or badly shaped

The read volume is high and the answer rarely changes

The honest test before you add a cache: have you fixed the slow query? A missing index is cheaper than a cache and does not go stale. Add the cache when the query is already good and the volume still hurts.

Pick serverless when the workload is new, spiky, or hard to size. Pick node-based when traffic is predictable, you want fine control over node type and placement, or you need one of the node-only features. The docs also note that serverless Valkey 9.0 can be reached over the internet without a VPC, which is handy from a laptop or CI job, and node-based clusters cannot.

What it costs

Serverless bills two dimensions. From the pricing page in US East (N. Virginia): $0.084 per GB-hour of data stored and $0.0023 per million ECPUs for Valkey. The minimum metered storage is 100 MB per cache for Valkey, against 1 GB for Memcached and Redis OSS. Valkey is priced 33% lower than Redis OSS on serverless and 20% lower on node-based.

Do the arithmetic once, because it surprises people in both directions. A cache holding 100 MB costs 0.1 GB x $0.084 = $0.0084 per hour, about $6.13 for a 730-hour month, before any requests. A million small reads cost $0.0023. So an idle-ish cache costs about six dollars and a busy small-object cache costs almost nothing in compute. The expensive dimension is memory: one GB stored for a month is about $61.32 (0.084 x 730). Node-based memory is usually cheaper per GB at steady load, so serverless wins on spiky or small workloads and loses on a large, flat, well-understood one. Price your actual working set against node sizes before choosing.

The setting that bites is the minimum. In the scaling documentation, you can set minimum ECPUs per second and minimum storage to pre-scale for a known peak. Once applied, you are billed for the minimum even if usage is lower. The doc's example, a 100,000 ECPU/second minimum, quotes $1.224 per hour. That figure matches the Redis OSS ECPU rate; at the Valkey rate, my own arithmetic (100,000 x 3,600 seconds x $0.0023 per million) gives about $0.83 per hour. Either way it is not free, so set minimums for events, then lower them. Lowering takes effect immediately.

Other line items: backup storage is $0.085 per GiB-month, there is no charge to create or restore a backup, and serverless with a VPC endpoint has no in-Region data transfer charge on the cache side. Node-based cross-AZ traffic is charged to your application at $0.01/GB. Free tier: accounts created before July 15, 2025 get 750 hours of cache.t3.micro for 12 months, which excludes serverless; accounts created after that get credits instead. Treat serverless as paid from the first hour.

The limits that bite

From the configuration and limits page, serverless caps are: 5,000 GiB per cache, 32 GiB per slot, 15,000,000 ECPUs per second per cache, and 30K ECPUs per second per slot (90K with Read from Replica). Keys and channel names can be 4 KiB, a single element request can be up to 512 MiB, 65,000 clients can connect at once, and a serverless cache supports up to 1024 databases.

The limit that matters in practice is not on that list. It is scaling speed. The troubleshooting guide says serverless can take 10 to 12 minutes to double its storage or request rate, and the scaling page says pre-scaling by changing minimums takes up to 60 minutes. A flash sale that goes from idle to a spike in 30 seconds can outrun the cache and produce throttled commands, visible as the ThrottledCmds metric. Most clients retry throttling automatically, so occasional throttling is fine; sustained throttling means you scaled late or you have a hot slot.

Two behaviors when you set your own maximums. If you hit the storage maximum, ElastiCache evicts keys that have a TTL using LRU; if nothing can be evicted, writes fail with an out-of-memory error. If you hit the ECPU maximum, requests are throttled. The docs recommend CloudWatch alarms at 75% of each configured maximum. The practical rule that follows: put a TTL on every key you write to a cache, because keys without one are the ones that can never be evicted.

Build it

We will build a cache-aside layer in Python. A fake database takes 50 ms per read. A cache sits in front, entries get a TTL with jitter, a lock prevents a stampede on one hot key, and at the end we compare the hit rate we computed on the client with the CacheHitRate CloudWatch reports.

Code below follows the current AWS documentation. I did not execute it against an account while writing this, so run it in a sandbox and trust your terminal over my prose.

Prerequisites. An AWS account and a sandbox VPC with at least two subnets in different Availability Zones. A machine inside that VPC to run the client (an EC2 instance, or CloudShell attached to the VPC), because the default serverless endpoint is a VPC endpoint. Python 3.10 or later, boto3, and redis (the redis-py client, which supports cluster mode and TLS).

python -m venv .venv && source .venv/bin/activate
pip install boto3 redis
aws sts get-caller-identity

IAM. For a sandbox, the managed policy AmazonElastiCacheFullAccess plus read access to EC2 networking and cloudwatch:GetMetricStatistics is enough. The scoped version needs elasticache:CreateServerlessCache, DescribeServerlessCaches, ModifyServerlessCache, and DeleteServerlessCache, plus EC2 describe permissions for subnets and security groups. The first serverless cache in an account depends on the ElastiCache service-linked role; if it is missing, the API fails with ServiceLinkedRoleNotFoundFault.

Step 1: create the cache with spending caps. Set the maximums at creation so a runaway loop cannot become a bill. The engine value is valkey.

import boto3, time
ec = boto3.client("elasticache", region_name="us-east-1")
NAME = "sl133-demo"
ec.create_serverless_cache(
    ServerlessCacheName=NAME,
    Engine="valkey",
    SubnetIds=["subnet-aaaa1111", "subnet-bbbb2222"],
    SecurityGroupIds=["sg-0123456789abcdef0"],
    CacheUsageLimits={
        "DataStorage": {"Maximum": 2, "Unit": "GB"},
        "ECPUPerSecond": {"Maximum": 10000},
    },
)
while True:
    c = ec.describe_serverless_caches(ServerlessCacheName=NAME)["ServerlessCaches"][0]
    print(c["Status"])
    if c["Status"] in ("available", "create-failed"):
        break
    time.sleep(10)
print(c["Endpoint"])

Replace the subnet and security group IDs with yours. The security group must allow inbound TCP 6379 and 6380 from the security group of your client machine; the troubleshooting guide names both ports for serverless. Status values in the API reference are listed in capitals (CREATING, AVAILABLE) while the CLI example reports lowercase, so compare case-insensitively in real code. The response carries an Endpoint and a ReaderEndpoint. Automatic snapshots are off unless you set DailySnapshotTime, so this cache incurs no backup storage.

Step 2: connect with TLS and cluster support. Serverless requires both. A client that does not use TLS hangs until it times out, and a client without cluster support fails with CROSSSLOT or SELECT errors.

from redis.cluster import RedisCluster
host = c["Endpoint"]["Address"]
r = RedisCluster(host=host, port=6379, ssl=True, decode_responses=True,
                 read_from_replicas=True)
print(r.ping())
r.set("hello", "world", ex=60)
print(r.get("hello"))

Expected output is True then world. The read_from_replicas option is what lets a single slot go past 30K ECPUs per second toward the 90K Read from Replica ceiling, at the cost of reads that can lag a write by a few milliseconds. If ping hangs, jump to the next section.

Step 3: write the cache-aside layer. The caching strategies guide describes lazy loading: check the cache, on a miss query the database, write the result back, return it. Each miss costs three trips, which is the price of only caching what is requested. We add two things the guide recommends or implies: a TTL, and a lock so one request repopulates a hot key while the rest wait briefly. The jitter keeps a batch of keys written together from expiring together.

import json, random, time
DB_CALLS = 0
def slow_db_read(item_id):
    global DB_CALLS
    DB_CALLS += 1
    time.sleep(0.05)                      # pretend this is Postgres
    return {"id": item_id, "name": f"item-{item_id}"}
def get_item(item_id, ttl=300):
    key = f"item:{item_id}"
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached), True
    lock = f"lock:{key}"
    if r.set(lock, "1", nx=True, ex=5):   # one request refills the key
        try:
            value = slow_db_read(item_id)
            r.set(key, json.dumps(value), ex=ttl + random.randint(0, 60))
            return value, False
        finally:
            r.delete(lock)
    time.sleep(0.05)                      # someone else is refilling
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached), True
    return slow_db_read(item_id), False   # give up waiting, hit the DB

Every command touches a single key, so there is no CROSSSLOT risk. The lock key and the item key land in different slots, which is fine because we never use them in one command. The lock expires in five seconds, so a crashed worker cannot wedge a key. This is a pragmatic stampede guard, not a distributed-lock proof; for money or inventory, do not use it.

Step 4: generate traffic and measure. Real traffic is skewed: a few items are very popular. We simulate that with a Zipf-like distribution over 500 items and send 3,000 requests.

import random
random.seed(1)
weights = [1 / (i + 1) for i in range(500)]
ids = random.choices(range(500), weights=weights, k=3000)
hits = misses = 0
t0 = time.time()
for i in ids:
    _, was_hit = get_item(i)
    hits += was_hit
    misses += (not was_hit)
elapsed = time.time() - t0
print(f"hits={hits} misses={misses} hit_rate={hits/(hits+misses):.1%} "
      f"db_calls={DB_CALLS} elapsed={elapsed:.1f}s")

On a skewed distribution like this you should see a hit rate far above 50% after the first few hundred requests, and DB_CALLS far below 3,000. Without a cache the loop would take about 150 seconds (3,000 x 50 ms). The exact numbers depend on the random draw, so do not expect to match mine digit for digit; the point is the shape.

Step 5: verify against CloudWatch. Client-side counters tell you what you think happened. CloudWatch tells you what the cache saw. The namespace is AWS/ElastiCache and the dimension is clusterId, set to the serverless cache name. The metrics page defines CacheHitRate as cache_hits / (cache_hits + cache_misses) over read-only key lookups, so it will differ from your number: our lock check and GET after a miss are lookups too.

from datetime import datetime, timedelta, timezone
cw = boto3.client("cloudwatch", region_name="us-east-1")
now = datetime.now(timezone.utc)
for metric, stat in [("CacheHitRate", "Average"), ("CacheHits", "Sum"),
                     ("CacheMisses", "Sum"), ("ElastiCacheProcessingUnits", "Sum"),
                     ("ThrottledCmds", "Sum")]:
    pts = cw.get_metric_statistics(
        Namespace="AWS/ElastiCache", MetricName=metric,
        Dimensions=[{"Name": "clusterId", "Value": NAME}],
        StartTime=now - timedelta(minutes=30), EndTime=now,
        Period=300, Statistics=[stat])["Datapoints"]
    print(metric, sorted((p["Timestamp"].strftime("%H:%M"), p[stat]) for p in pts))

Metrics can lag a few minutes, so rerun if the list is empty. Success looks like a nonzero CacheHits sum, a CacheHitRate in the same neighborhood as your client number, and ThrottledCmds at zero or near it. If ThrottledCmds is climbing, you hit the 10,000 ECPU per second cap we set in Step 1; raise it with modify_serverless_cache.

Now do the thing that teaches you the most: change ttl to 5, rerun, and watch the hit rate drop and ECPU and DB calls rise. That tradeoff, freshness against load, is the whole product.

When it breaks

Connection hangs, then times out. The client is not using TLS, or the client machine sits in a different VPC, or the security group does not allow 6379 and 6380 from the client. All three look identical from the client, so check TLS first, then network.

CROSSSLOT Keys in request don't hash to the same slot. A multi-key command, transaction, or Lua script touches keys in different slots. Use hash tags so related keys share a slot, for example {user:42}:profile and {user:42}:cart, or stop issuing multi-key commands across unrelated keys.

CROSSSLOT or SELECT errors on every command. Your client is not cluster aware. Serverless is always cluster mode; use a cluster client such as RedisCluster. If you need cluster mode disabled, you need a node-based cluster.

Throttled commands. Either the workload outgrew the cache faster than the 10 to 12 minute doubling time, or one slot is above about 30K ECPUs per second. Spread hot keys, enable Read from Replica in the client, and pre-scale with minimums an hour ahead of a known peak.

Writes fail with an out-of-memory error. You reached your configured storage maximum and nothing had a TTL to evict. Add TTLs and raise the maximum, and alarm at 75%.

ServiceLinkedRoleNotFoundFault on create. The ElastiCache service-linked role is missing in the account. Create it once, then retry.

Cleanup

Delete the cache, then confirm it is gone and that the security group and any test instance you created are removed.

ec.delete_serverless_cache(ServerlessCacheName=NAME)
while True:
    try:
        s = ec.describe_serverless_caches(ServerlessCacheName=NAME)["ServerlessCaches"][0]["Status"]
        print(s)
        time.sleep(10)
    except ec.exceptions.ServerlessCacheNotFoundFault:
        print("deleted")
        break

We never set a final snapshot or automatic snapshots, so there is no backup storage to clean. Delete the security group and the client instance if you made them only for this exercise. Expected cost for a one-hour run: roughly $0.01 for storage at the 100 MB minimum (0.1 GB x $0.084) plus a fraction of a cent for a few thousand small requests. Check Cost Explorer the next day and confirm ElastiCache shows nothing running.

Where the bet pays off

Run the Step 4 loop with three TTLs and plot hit rate against database calls. Then add the invalidation you have been ignoring: on every database write, delete the cache key in the same code path, and see how many places in your real codebase write to that table. That count is the true cost of the cache.

Once the hit-rate number is yours, the decision between serverless and nodes becomes arithmetic instead of taste. A cache with a 95% hit rate in front of a 50 ms query is doing real work. A cache with a 30% hit rate is mostly a way to pay for memory, and the cheapest fix may be an index.

Sources