ElastiCache and OpenSearch Cost Optimization: Nodes, Tiers and Shards

ElastiCache and OpenSearch cost optimization: Graviton and reserved nodes, r6gd data tiering, Serverless break-even, UltraWarm and cold storage, shards and ISM.

ElastiCache and OpenSearch Service sit in the same corner of most AWS bills: sized once during a launch, never touched again, and running 730 hours a month. A staging cache gets a replica “for safety”. A log domain keeps a year of indexes on EBS, each with a replica, because that was the default. Here’s how we work through both, with us-east-1 list prices at the time of writing where we quote one.

Where the money goes on both bills

ElastiCache node-based clusters bill per node-hour, and every replica is a full node. Backup storage beyond one free snapshot per cluster is $0.085 per GB-month. ElastiCache Serverless bills instead for data stored in GB-hours and for ElastiCache Processing Units (ECPUs), one per KB moved by a read or write. Reserved nodes discount node-hours only.

OpenSearch Service bills per instance-hour for data, dedicated master and UltraWarm nodes; per GB-month for EBS attached to data nodes; and per GB-month of managed storage for UltraWarm and cold data. Automated snapshots are free. Reserved Instances discount instance-hours only. Pull the last full month by usage type for both before touching anything (Cost Explorer charges $0.01 per API call):

# Read-only: last full month of ElastiCache and OpenSearch spend by usage type
START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%F 2>/dev/null || date -u -v1d -v-1m +%F)
END=$(date -u +%Y-%m-01)
aws ce get-cost-and-usage \
  --time-period Start="$START",End="$END" \
  --granularity MONTHLY --metrics UnblendedCost \
  --filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon ElastiCache","Amazon OpenSearch Service"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE \
  --query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' \
  --output text | sort -t$'\t' -k2 -rn | head -25

Usage types beginning with NodeUsage are ElastiCache node-hours and ESInstance lines are OpenSearch instance-hours.

Size ElastiCache nodes by memory, not CPU

A cache is a memory product. The r families give about 8 GiB per vCPU (a cache.r6g.large has 2 vCPUs and 13.07 GiB); the m families about 4 GiB per vCPU (a cache.m6g.large has 6.38 GiB). Picking an m node and then sizing up for memory a month later is how people end up paying for CPU they never use.

Redis OSS and Valkey execute commands on one thread, which is why ElastiCache publishes EngineCPUUtilization separately from CPUUtilization: on a 4-vCPU node, a host-level 25% can mean the engine thread is pinned. Memcached is multi-threaded, so there CPU does scale with the node. For memory, look at DatabaseMemoryUsagePercentage and Evictions, remembering that reserved-memory-percent holds back 25% of the node for backups and replication by default. Pull two weeks of peaks per node:

# Read-only: 14-day peak and average memory and engine CPU for one node
CLUSTER=prod-cache-001   # the CacheClusterId of a member node, not the replication group
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
START=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-14d +%Y-%m-%dT%H:%M:%SZ)
for m in DatabaseMemoryUsagePercentage EngineCPUUtilization; do
  aws cloudwatch get-metric-statistics --namespace AWS/ElastiCache --metric-name "$m" \
    --dimensions Name=CacheClusterId,Value="$CLUSTER" Name=CacheNodeId,Value=0001 \
    --start-time "$START" --end-time "$END" --period 1209600 --statistics Maximum Average \
    --query "[\`$m\`, Datapoints[0].Maximum, Datapoints[0].Average]" --output text
done

A node whose peak memory sits under 40% and whose engine CPU never passes 20% can drop a size; the modify is online for Redis OSS and Valkey. If evictions are climbing, go the other way, to a bigger r node.

Graviton, Valkey and reserved nodes

Three changes stack here, in this order.

Graviton first. The r6g, m6g, r7g and m7g node types cost less per hour than the x86 node of the same size, and the move is a node-type modify on an existing cluster. If you’re still on r4, r5, m4 or m5 nodes, this is the easiest line on the bill to shrink; our Graviton and rightsizing guide covers the compatibility checks, which for a managed cache are almost none.

Valkey second. ElastiCache for Valkey is priced 20% below Redis OSS on node-based clusters and 33% below on Serverless. Upgrading a Redis OSS 7.x cluster is an in-place engine modify and the protocol is the same, so most applications don’t notice. Do it before buying reservations, so the commitment matches what you’ll actually run.

Reserved nodes last. ElastiCache reserved nodes come in one- and three-year terms with no, partial or all upfront payment, and AWS puts the saving at up to 55% against on-demand. They apply to node-based clusters only, not Serverless. Buy them for the floor: nodes that will still exist in twelve months, in the type you’ve already migrated to. aws elasticache describe-reserved-cache-nodes shows what you own; a reservation for a cache.r5.large you replaced with an r6g is wasted until it expires. Confirm the current size-flexibility rules on the reserved nodes page before you commit, and size it the way we do in Reserved Instances vs Savings Plans: cover the base, leave the peak on-demand.

This inventory lists every node, then every replication group’s Multi-AZ, failover and tiering settings:

# Read-only: every ElastiCache node, then every replication group's layout
aws elasticache describe-cache-clusters --show-cache-node-info \
  --query 'CacheClusters[].[CacheClusterId,Engine,EngineVersion,CacheNodeType,NumCacheNodes,PreferredAvailabilityZone,ReplicationGroupId]' \
  --output table
aws elasticache describe-replication-groups \
  --query 'ReplicationGroups[].[ReplicationGroupId,CacheNodeType,MultiAZ,AutomaticFailover,DataTiering,ClusterEnabled,length(NodeGroups),length(MemberClusters)]' \
  --output table

Data tiering with r6gd nodes

If a cache holds a lot of data but reads most of it rarely, you’re paying memory prices for cold values. Data tiering on the cache.r6gd family keeps every key in memory and moves the least recently used values to the node’s local NVMe SSD. AWS’s figure is over 60% lower cost per GB of capacity than r6g nodes at full utilization.

The conditions: Redis OSS 6.2 or later or Valkey 7.2 or later, cache.r6gd.xlarge or larger, and the choice is made at creation, so an existing group is replaced by restoring a snapshot into a new r6gd group. AWS recommends it when 20% or less of the dataset is accessed regularly. Reads from SSD take longer, so it isn’t for a cache where every key is hot.

ElastiCache Serverless and where it loses

Serverless is the right default for new, small or unpredictable caches: nothing to size, Multi-AZ from the start. Redis OSS costs $0.125 per GB-hour stored and $0.0034 per million ECPUs; Valkey is a third lower at $0.084 and $0.0023. The minimum billed storage is 1 GB for Redis OSS and 100 MB for Valkey, so an idle Valkey cache is about $6 a month and an idle Redis OSS cache about $91. Where it loses is a cache that is steadily full. Take 10 GB held all month:

Option Monthly cost
Serverless, Redis OSS, 10 GB stored $912.50 before ECPUs
Serverless, Valkey, 10 GB stored $613.20 before ECPUs
cache.r6g.large (13.07 GiB) primary plus one replica, on-demand about $300

A cache.r6g.large lists around $0.21 an hour on-demand; check it on the pricing page, but the ratio is what matters. ECPUs add to the Serverless side: 5,000 one-KB commands a second is about 13 billion ECPUs a month, roughly $44 on Redis OSS; a 10 KB value is 10 ECPUs per read, which adds up faster.

Our rule: Serverless under about 2 GB, for bursty or unknown traffic, and for dev and test; provisioned Graviton nodes with a reservation for anything holding more than a few GB around the clock. Measure BytesUsedForCache and ElastiCacheProcessingUnits for a week first.

OpenSearch: hot storage is the expensive part

On a search domain, instance-hours dominate. On a log domain, EBS storage plus the data nodes needed to hold it dominate, and the two are coupled: each instance type caps how much EBS it can attach, so more retention means more nodes even when the CPUs are idle. Three things move that bill:

  • gp3 over gp2. gp3 is about 10% cheaper per GB, includes a 3,000 IOPS and 125 MiB/s baseline, and raises the storage cap per node for most instance types, so a storage-bound domain can run fewer nodes. It’s an EBS options modify.
  • Graviton data nodes. m6g, r6g and c6g .search types are cheaper per hour than the m5, r5 and c5 types they replace, and a blue/green deployment handles the swap without downtime.
  • OR1 for ingest-heavy logs. OR1 instances (OpenSearch 2.11 and later) keep a copy of each index in S3-backed storage, and AWS claims up to 30% better price-performance for indexing-heavy workloads.

OpenSearch Reserved Instances come in one- and three-year terms with the usual payment options and cover instance-hours for the types you specify. aws opensearch describe-reserved-instances shows what you own and describe-reserved-instance-offerings what’s for sale. Reserve after the Graviton and gp3 changes, not before. Start with the layout of every domain:

# Read-only: every domain's engine, nodes, masters, AZ layout, tiers and storage
for d in $(aws opensearch list-domain-names --query 'DomainNames[].DomainName' --output text); do
  aws opensearch describe-domain --domain-name "$d" --query \
    'DomainStatus.[DomainName, EngineVersion, ClusterConfig.InstanceType, ClusterConfig.InstanceCount, ClusterConfig.DedicatedMasterEnabled, ClusterConfig.ZoneAwarenessEnabled, ClusterConfig.MultiAZWithStandbyEnabled, ClusterConfig.WarmEnabled, ClusterConfig.WarmType, ClusterConfig.WarmCount, ColdStorageOptions.Enabled, EBSOptions.VolumeType, EBSOptions.VolumeSize]' \
    --output text
done

VolumeSize is per data node; multiply by InstanceCount for the GB-month you’re paying.

UltraWarm and cold storage

UltraWarm is where log retention should live after the first few days. Indexes moved to warm become read-only, are stored once in S3-backed managed storage at $0.024 per GB-month, and are served by ultrawarm1.medium.search or ultrawarm1.large.search nodes that cache the hot parts. You pay for primary shard size only; warm data needs no replicas because S3 provides the durability.

That’s why the saving is bigger than the per-GB prices suggest. Hot data on EBS is usually stored twice (primary plus replica) at several times the per-GB rate of managed storage, so the gap per GB of indexed data is roughly ten to one; check the EBS figure on the OpenSearch pricing page. The warm nodes are the fixed cost: a medium holds up to 1.5 TiB and a large up to 20 TiB. UltraWarm needs dedicated master nodes.

Cold storage detaches data from compute entirely and bills managed storage alone. To search it you migrate the index back to warm first, which takes minutes, so it suits the “keep 13 months for the auditors” tier. It requires UltraWarm on the domain. If the requirement is only to retain raw logs, not search them, shipping them to S3 is cheaper still; our CloudWatch Logs to S3 article covers that pattern.

Shards, replicas and ISM policies

The quiet cost on OpenSearch isn’t data, it’s shard count. Every shard is a Lucene index with its own heap overhead, and a data node’s JVM heap is half its RAM up to about 32 GiB. AWS’s guidance is 10 to 30 GiB per shard for search, 30 to 50 GiB for logs, and under about 25 shards per GiB of heap. A domain that writes one index per day per application, each with the old Elasticsearch default of five primaries plus a replica, ends up with thousands of shards a few hundred MB each. The heap fills long before the disks do, and someone adds nodes. See what you have (the endpoint needs whatever authentication you’ve configured):

# Read-only: largest indexes with primary and replica counts, then disk per node
ENDPOINT=search-logs-abc123.us-east-1.es.amazonaws.com
curl -s "https://$ENDPOINT/_cat/indices?v&h=index,health,pri,rep,docs.count,pri.store.size,store.size&s=store.size:desc" | head -25
curl -s "https://$ENDPOINT/_cat/allocation?v&h=node,shards,disk.indices,disk.used,disk.avail,disk.percent"

Fixes, in order of impact:

  1. Roll over by size, not by day. One primary shard per data node and a rollover at 30 to 50 GiB or one day, whichever comes first.
  2. Replicas: one in production, zero in dev. Each replica doubles storage and indexing work.
  3. Shrink the legacy indexes with five primaries and 2 GB of data; the _shrink API does it without reindexing. Set index.codec: best_compression on log indexes while you’re there.

Then make retention automatic with an ISM policy. For logs we use four states: hot with a rollover action; warm after 3 to 7 days with warm_migration; cold after 30 to 90 days with cold_migration; and delete at the retention limit. Attach it through the index template’s plugins.index_state_management.policy_id setting so new indexes pick it up. A policy with no delete state is a storage bill with no ceiling, and we see those often.

Multi-AZ, dedicated masters and the dev clusters nobody stopped

Multi-AZ is the right default for anything an outage would page you about and the wrong default for everything else, because on both services it’s paid for in whole nodes.

On ElastiCache, Multi-AZ with automatic failover needs at least one replica, and each replica is a full node-hour; a staging Redis group with one primary and two replicas costs three times the single node it needs. Memcached has no replication at all, which is often all a cache needs.

On OpenSearch, zone awareness across two AZs needs at least one replica. Multi-AZ with Standby adds a third AZ whose data nodes take no traffic until a failover, so you run 50% more data nodes than a two-AZ domain of the same serving capacity, and three dedicated masters are three more instance-hours. A dev domain needs none of it: one AZ, zero replicas, no masters, one small node.

Neither service can be stopped. For ElastiCache, take a final snapshot and delete the cluster; snapshots cost $0.085 per GB-month beyond the free copy, and dev caches that must stay reachable belong on Serverless Valkey at its 100 MB minimum. For OpenSearch, take a manual snapshot to your own S3 bucket and delete the domain, or shrink dev to one t3.small.search node with zero replicas.

The clusters that cost most are the ones nobody remembers creating. A Redis group with CurrConnections at zero for a week, or a domain with SearchRate and IndexingRate flat at zero, is a finding, not a resource; our idle and orphaned resources guide has the wider sweep.

FAQ

Should I use ElastiCache Serverless or provisioned nodes?

Serverless for small, bursty or unknown workloads and for dev and test. Provisioned Graviton nodes with a reservation once the cache holds more than a few GB around the clock, because Serverless storage per GB-hour costs several times what a full node’s memory does.

Do ElastiCache reserved nodes apply to Serverless?

No. Reserved nodes discount node-hours on node-based clusters only.

How much cheaper is Valkey than Redis OSS on ElastiCache?

AWS prices Valkey 20% below Redis OSS on node-based clusters and 33% below on Serverless. The upgrade from Redis OSS 7.x is an in-place modify.

What’s the difference between OpenSearch UltraWarm and cold storage?

UltraWarm keeps read-only indexes in managed storage served by warm nodes, so they stay searchable. Cold storage drops the nodes and bills storage alone; an index must move back to warm before it can be searched.

Can I stop an ElastiCache cluster or OpenSearch domain?

No. Snapshot and delete, then restore when needed. For dev caches that must stay up, use Serverless Valkey.

Run the two inventories first

Run the two inventory commands above and put the output in a sheet, one row per cluster or domain. Mark every dev or staging row, every production row with replicas or Multi-AZ it can’t justify, and every domain holding more than a week of logs on EBS. The first group is usually the biggest saving and needs nothing beyond a snapshot and a delete; UltraWarm and ISM come second; reservations last, once the fleet has stopped moving.

Kostrix runs the same sweep weekly: its read-only idle check lists ElastiCache clusters with no connections and OpenSearch domains with no search or indexing traffic over the scan window, and its reservation coverage check shows which node-hours on the bill are still on-demand.