Home› Blog› Week 27 - ElastiCache for Valkey: Twenty Thousand …
AWS Weekly Lab AWS Terraform

Twenty thousand requests, three different bills

A cache priced per request would be easy to budget. ElastiCache Serverless is not priced per request, and the number everyone watches on the dashboard turns out to predict almost nothing about the invoice.

Verified against current vendor documentation on 13 November 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
AWS Platform Engineering Lab · Week 27 of 52

Lab Overview

A courier charges by the parcel. You send a hundred parcels, you pay for a hundred. Then you discover the invoice is by weight, and the hundred envelopes you sent last month cost rather less than the hundred boxes you sent this month.

Nothing about the count changed. The count was never the thing being charged for.

The problem. Capacity conversations about a cache start with requests per second, because that is the number on every dashboard. ElastiCache Serverless bills in ECPUs, and an ECPU is charged per kilobyte moved. The request rate and the bill are related only through a quantity nobody is looking at.

The objective. Issue the same number of requests three times, against values of 1 KB, 10 KB and 100 KB, and read the billed ECPUs for each from CloudWatch.

The outcome. 20,000 requests billed 20,803, 200,830 and 2,000,960 ECPUs — a 96× spread with the request count held constant. The byte model predicts all three to within 4%, which is the point: the model is sound and the request count is the useless number.

DifficultyBeginner to intermediate
Measured cost$0.078 from Cost Explorer, of which $0.065 was the idle storage floor
DeploymentTerraform via HCP Terraform with OIDC dynamic credentials
Regionus-east-1 — a serverless cache is reachable only from its own VPC
Full lab guideGitHub — prerequisites, Terraform, commands, validation, cleanup

Cloud Services and Concepts

ServicePurpose hereWhat it is
Amazon ElastiCache ServerlessThe cache under test, and the thing doing the billing A managed in-memory cache with no nodes to size. You are charged for data stored and for the work your requests do, rather than for a server by the hour.
ValkeyThe engine — version 9.0, the current default An open-source key-value store, forked from Redis in 2024 and now the engine AWS prices lowest. Same commands, same clients.
AWS LambdaThe load generator, inside the VPC because the cache has no public endpoint Runs code on demand with no server to manage. Attaching it to a VPC is what lets it reach private resources.
Amazon CloudWatchElastiCacheProcessingUnits — the only figure here that is a measurement rather than a calculation AWS's metrics service. Most services publish numbers into it without being asked.
Amazon VPCRequired — a serverless cache is reachable only from inside one A private network inside your account. Nothing in it is reachable from the internet unless you arrange it.
HCP TerraformRemote applies using OIDC, so no AWS keys are stored anywhere HashiCorp's hosted Terraform. It runs plans and applies on its own machines and keeps the state file.

An ECPU is the higher of two numbers

AWS defines it in two parts. A simple read or write "requires 1 ECPU for each kilobyte (KB) of data transferred". A command that needs more processing consumes proportionally more. And when both apply, "ElastiCache calculates the ECPUs required based on the higher of the two dimensions" — not the sum.

For a plain GET the byte dimension wins, which makes the arithmetic simple and the budgeting counter-intuitive: a 100 KB value costs a hundred times a 1 KB value to read, and the request counter shows the same number either way.

Valkey's floor is 100 MB, not 1 GB

Storage is billed per GB-hour with a minimum, and the minimum differs by engine: 100 MB for Valkey against 1 GB for Redis OSS and Memcached. At $0.084/GB-hour that is about $6.13 a month for a cache that holds almost nothing, which is the figure behind AWS's "starts at $6/month". The same page says there is "no minimum fee". Both statements are true: there is no fee floor, but there is a storage floor.

Architecture and Workflow

aws AWS Cloud Region: us-east-1 VPC — a serverless cache has no public endpoint load client 20,000 reads, each run Icon-Architecture/64/Arch_Amazon-ElastiCache_64 Valkey 9.0 serverless 1 2 3 1 KB 10 KB 100 KB SAME REQUEST COUNT, BILLED: 1 1 KB 20,803 ECPUs 2 10 KB 200,830 ECPUs 3 100 KB 2,000,960 ECPUs 96x the cost. 20,000 requests either way. An ECPU is charged per kilobyte moved, not per request.
Three runs against one cache. The request count is held at 20,000 throughout; only the value size changes.
  1. 20,000 reads of 1 KB values. The byte dimension implies 20,000 ECPUs.
  2. 20,000 reads of 10 KB values. Same request count, ten times the bytes.
  3. 20,000 reads of 100 KB values. Same request count again, a hundred times the bytes.

Each run writes its value once, then reads it back 20,000 times sequentially. A cache hit is the cheapest thing the service does, so this is the best case for a request-based estimate — and it still misses by two orders of magnitude.

Prerequisites

RequirementWhyHow to check
Terraform ≥ 1.10cloud {} block and AWS provider v6terraform version
HCP Terraform workspace, CLI-drivenThe Lambda package is built locally and uploaded with the configurationterraform init
AWS permissions for ElastiCache, Lambda, VPC, IAMThe apply creates all fouraws sts get-caller-identity
Python 3.11+ with pipBuilds the package — valkey-py is not in the Lambda runtimepython --version
AWS CLI v2Teardown is verified against the APIaws --version

Implementation

What was deployed

ResourceHow manyIts job
ElastiCache Serverless cache1Valkey 9.0, storage capped at 2 GB and 15,000 ECPUs/second
Lambda function1The load generator, VPC-attached
VPC and subnets1 / 2Two AZs, because the cache requires a subnet in more than one
Security groups2Client egress only; cache accepts 6379 from the client alone
IAM role and policy1 / 1Logs, plus the EC2 permissions Lambda needs to build its VPC network interface
CloudWatch log group17-day retention

Ten resources, applied in about seven minutes.

The HCP Terraform run page for week-27-dev showing the apply finished with ten resources created, triggered via CLI.
The apply, run remotely by HCP Terraform using OIDC dynamic credentials — the workspace holds a role to assume, not an access key.
The ElastiCache console showing one Valkey cache named week27-valkey, status Available, engine version 9.0, configuration Serverless.
One serverless cache, engine version 9.0. There is no node type to choose and no node count, which is exactly why the bill has to be expressed in something other than instance-hours.

The one thing that is not obvious in the Terraform

Encryption in transit is not a choice. AWS: "If you are using ElastiCache Serverless, encryption in transit is always enabled", and it cannot be turned off. The client therefore has to speak TLS or it will not connect at all:

_client = valkey.Valkey(host=HOST, port=PORT, ssl=True,
                        ssl_cert_reqs=None, socket_timeout=10)

Port 6379 is the primary endpoint. 6380 is a read-optimised reader endpoint, left closed here so the measurement has one path.

Validation and Evidence

Three runs, 20,000 reads each, separated by five idle minutes so no measurement window overlaps another. Billed ECPUs are read from CloudWatch's ElastiCacheProcessingUnits after every run has finished and published.

TestExpectedActualStatus
20,000 reads at 1 KB~20,000 ECPUs20,803 billed✅
20,000 reads at 10 KB~200,000 ECPUs200,830 billed✅
20,000 reads at 100 KB~2,000,000 ECPUs2,000,960 billed✅
Spread at constant request countabout 100×96×✅
BytesUsedForCacheunder the 100 MB floora few hundred KB✅
A terminal showing the three runs: 20,000 requests each, with 20,803, 200,830 and 2,000,960 ECPUs billed and costs of $0.000048, $0.000462 and $0.004602.
The estimate and the measurement side by side. They agree, which is what makes the request column meaningless rather than merely imprecise.
A CloudWatch graph of ElastiCacheProcessingUnits per minute over 35 minutes, showing two barely visible deflections and one spike reaching about 1.45 million.
The same three runs as CloudWatch saw them. The 1 KB and 10 KB runs are almost invisible against the 100 KB spike — and all three moved exactly 20,000 requests.
A CloudWatch graph of BytesUsedForCache showing stored data far below the 100 MB minimum billed storage.
Stored data never approached the 100 MB minimum. The storage line of this bill was the floor, not the contents.

Security and Cost

What this means for a platform team

Size a cache from payload, not from request rate. The request-per-second figure on the dashboard is the number everyone quotes in a capacity review, and on ElastiCache Serverless it carries almost no information about cost. Two services with identical traffic profiles can differ by two orders of magnitude on the invoice because one of them caches rendered HTML and the other caches integers.

The practical control is the value itself. Caching a 100 KB serialised object where a 1 KB identifier would do is not a style preference at this price; it is the dominant term.

On security, two things are worth copying regardless of engine: the cache has no public endpoint and cannot be given one, and encryption in transit is mandatory and cannot be disabled. Neither needed configuring — which is the right default for a datastore.

Cost

LineRate (us-east-1, verified 13 November 2026)
Storage$0.084 per GB-hour, minimum 100 MB billed for Valkey
ECPUs$0.0023 per million
VPCFree — no NAT gateway, the client sits with the cache

Measured: $0.078 from Cost Explorer, for a cache that existed about thirteen hours. Cost Explorer splits it in a way worth reading closely:

Usage typeQuantityCost
CachedData:Valkey0.773 GB-hours$0.0649
ElastiCacheProcessingUnits:Valkey5,633,206 ECPUs$0.0130

Storage was 83% of the bill, and almost all of that was the 100 MB floor on a cache holding a few hundred kilobytes.

The ECPU figure is larger than this experiment: the three measured runs account for 2.2 million of those 5.6 million, with the rest from earlier attempts that were discarded. Following this lab once costs about half a cent in ECPUs, plus the storage floor for as long as the cache exists.

That ratio is the lesson in miniature. The workload everyone would have budgeted for cost a fifth of the idle floor nobody would have thought to budget for.

Troubleshooting and Lessons

Problem. The first run died with ReadTimeoutError: Read timeout on endpoint URL while invoking the Lambda.
Root cause. Two layers. boto3 stops waiting for a response after 60 seconds by default, well short of the function's 300-second timeout; and 200,000 sequential round-trips genuinely exceeded that 300 seconds. CloudWatch recorded Duration: 300,000 ms and one error.
Resolution. An explicit read_timeout on the boto3 client, and 20,000 requests rather than 200,000.
Lesson. A serverless cache answers in about a millisecond and a loop of 200,000 of them does not finish in five minutes. Per-operation latency and total runtime are different questions.

Problem. The first measurement reported 10 KB values billing fewer ECPUs than 1 KB values.
Root cause. The CloudWatch windows were five minutes wide and overlapped each other, so each figure included a neighbouring run. A second attempt fixed the overlap but still queried too early and caught only the first of the two minutes each run spans.
Resolution. Idle gaps between runs, and the measurement taken after all three have finished publishing.
Lesson. Bytes moved set a floor on ECPUs, so a billed figure below that floor is impossible rather than surprising. The runner now refuses to print any result below it, which is what caught both attempts.

Problem. The remote apply failed with No valid credential sources found.
Root cause. The workspace is created on first terraform init and starts with no variables, so an HCP run has no way to reach AWS.
Resolution. Set TFC_AWS_PROVIDER_AUTH and TFC_AWS_RUN_ROLE_ARN on the workspace, which is how every other week in this series authenticates.
Lesson. A new workspace is not a configured workspace, and the error names the symptom rather than the cause.

Cleanup and Key Takeaways

Teardown is verified against the API rather than the destroy output — a destroy reports what Terraform removed, not what is left in the account. The cache and the function are both checked by name.

Key takeaways

Request count is the wrong input

20,000 requests billed 20,803 or 2,000,960 ECPUs depending only on value size. Capacity conversations that start with requests per second start in the wrong place.

Payload size is the design decision

At 1 ECPU per kilobyte, what you choose to put in the cache is the dominant cost term. Caching an identifier instead of a rendered object is a pricing choice.

"No minimum fee" is not "no floor"

Valkey bills a minimum 100 MB of storage — about $6.13 a month — against 1 GB for Redis OSS. For a small cache that minimum matters more than the headline discount.

A measurement that is impossible is not a measurement

Bytes moved set a floor on ECPUs. Knowing that floor is what exposed two bad measurement windows that otherwise produced plausible-looking tables.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent