Skip to content
chiltepin

Generated from: “Compare the three caching options we discussed: Redis, Memcached, and an in-process LRU.

Cache options for the Catalog API

Written by an agent from the skill, validated by chiltepin check, rendered by the renderer — shown as generated, 13 September 2026.

DOCUMENTDECISION

Cache options for the Catalog API

Redis, Memcached, and an in-process LRU compared against the product-detail read path.

The Catalog API serves product-detail reads at about 4,000 requests per second from six replicas. Each read joins three tables and costs 18 ms at p50 on Postgres. A cache in front of that query is the cheapest way to hold the 50 ms latency target while the catalog grows.

SECTION 01 · Note

Assumptions

Note
The workload is product-detail reads keyed by product id, hot set about 2 GB, TTL 5 minutes. Price changes must be visible on every replica within 2 seconds. The service runs six Node.js replicas on Kubernetes, and a managed Redis and a managed Memcached are both available in the same region. Numbers below come from a one-hour soak at 4,000 rps on the staging cluster.

Approaches explored

Each option holds the same 2 GB hot set and the same 5-minute TTL. The differences are where the data lives, how invalidation reaches every replica, and what the team operates.

SECTION 02 · Options

Three ways to cache product-detail reads

Option 1Redis
One managed Redis cluster shared by all replicas. Cache-aside reads; price changes publish an invalidation on a Redis channel.
  • One copy of the hot set, every replica sees the same value
  • Pub/sub carries invalidations in under 100 ms
  • Sorted sets and hashes fit the related-products list
  • Persistence survives a restart without a cold start
  • Every hit pays a network round trip
  • One more managed service to page on
  • Single-threaded command path caps a node near 100k ops/s
CHOSEN — meets the 2-second invalidation rule with the least memory
Option 2Memcached
One managed Memcached pool shared by all replicas. Cache-aside reads; price changes delete the key on every pool node.
  • Lowest per-hit latency of the network options
  • Multi-threaded, scales with cores
  • Smallest memory footprint per key
  • No pub/sub, so invalidation is a client-side fan-out
  • Values are opaque bytes, no partial updates
  • No persistence, so a pool restart is a cold start
VIABLE — kept as fallback if Redis throughput becomes the limit
Option 3In-process LRU
Each replica keeps its own bounded LRU map in Node.js heap. Price changes reach replicas through a Kafka topic that each replica consumes.
  • Microsecond hits, no network hop
  • No new service to run
  • Zero cost for the cache itself
  • Six copies of the hot set, 12 GB of replica memory
  • Each replica warms alone, so hit rate drops after every deploy
  • Invalidation depends on Kafka consumer lag, measured at up to 6 seconds
  • A 2 GB heap raises GC pause time
REJECTED — fails the 2-second invalidation rule and multiplies memory by replica count

Measured on the staging soak

The soak ran one hour at 4,000 rps with a price-change burst of 500 keys at the 30-minute mark. The hit-rate row reads the two minutes after that burst, which is where the options separate most.

SECTION 03 · Benchmark

One-hour soak, 4,000 rps, six replicas

MetricRedismanaged, 2 nodesMemcachedmanaged, 2 nodesIn-process LRUper replica
Hit latency p50
0.4 ms
0.3 ms
0.002 ms
Hit latency p99
1.8 ms
1.4 ms
0.01 ms
Hit rate2 min after price burst
94%
91%
62%
Invalidation lag p99price change to last replica
90 ms
140 ms
6.1 s
Memory for the hot setall replicas combined
2.6 GB
2.2 GB
12 GB
Hit rate after a rolling deployfirst 5 min
93%
90%
41%
Added monthly cost
$310
$240
$180

LRU cost is the replica memory increase from 1 GB to 3 GB per pod, not a cache service.

The LRU wins every latency row by three orders of magnitude and loses every row that involves more than one replica. Its 6.1 s invalidation lag alone rules it out under the 2-second rule. Redis and Memcached sit within 0.1 ms of each other on hits; Redis costs $70 more per month and buys pub/sub invalidation and persistence.

Where each option sits

SECTION 04 · Matrix

Operational cost against cross-replica consistency

2×2
QuadrantOperational cost →↑ Cross-replica consistencyNothing new to runNew service to page onOne value everywhereEach replica driftsRedisMemcachedIn-process LRU

The top-right holds both network caches; the price of consistency is a service to operate. The LRU sits alone in the bottom-left, cheap and inconsistent. Nothing lands in the top-left, which is the quadrant a single-replica service would enjoy and this one cannot.

Decision

SECTION 05 · Note

Redis, cache-aside, 5-minute TTL

Success
Adopt the managed Redis cluster for product-detail reads. Invalidate by publishing the product id on a Redis channel when a price changes; each replica subscribes and deletes the key. Revisit Memcached only if Redis command throughput passes 80k ops/s per node. Do not add an in-process LRU in front of Redis until a measured need appears, because the deploy-time hit-rate drop returns with it.
View the Markdown
```meta
title: Cache options for the Catalog API
subtitle: Redis, Memcached, and an in-process LRU compared against the product-detail read path.
tag: DECISION
```

The Catalog API serves product-detail reads at about 4,000 requests per second from six replicas. Each read joins three tables and costs 18 ms at p50 on Postgres. A cache in front of that query is the cheapest way to hold the 50 ms latency target while the catalog grows.

```callout
tone: note
title: Assumptions
body: "The workload is product-detail reads keyed by product id, hot set about 2 GB, TTL 5 minutes. Price changes must be visible on every replica within 2 seconds. The service runs six Node.js replicas on Kubernetes, and a managed Redis and a managed Memcached are both available in the same region. Numbers below come from a one-hour soak at 4,000 rps on the staging cluster."
```

## Approaches explored

Each option holds the same 2 GB hot set and the same 5-minute TTL. The differences are where the data lives, how invalidation reaches every replica, and what the team operates.

```options
title: Three ways to cache product-detail reads
items:
  - { kicker: Option 1, title: Redis, how: "One managed Redis cluster shared by all replicas. Cache-aside reads; price changes publish an invalidation on a Redis channel.", pros: ["One copy of the hot set, every replica sees the same value", "Pub/sub carries invalidations in under 100 ms", "Sorted sets and hashes fit the related-products list", "Persistence survives a restart without a cold start"], cons: ["Every hit pays a network round trip", "One more managed service to page on", "Single-threaded command path caps a node near 100k ops/s"], verdict: "CHOSEN — meets the 2-second invalidation rule with the least memory", tone: chosen }
  - { kicker: Option 2, title: Memcached, how: "One managed Memcached pool shared by all replicas. Cache-aside reads; price changes delete the key on every pool node.", pros: ["Lowest per-hit latency of the network options", "Multi-threaded, scales with cores", "Smallest memory footprint per key"], cons: ["No pub/sub, so invalidation is a client-side fan-out", "Values are opaque bytes, no partial updates", "No persistence, so a pool restart is a cold start"], verdict: "VIABLE — kept as fallback if Redis throughput becomes the limit", tone: viable }
  - { kicker: Option 3, title: In-process LRU, how: "Each replica keeps its own bounded LRU map in Node.js heap. Price changes reach replicas through a Kafka topic that each replica consumes.", pros: ["Microsecond hits, no network hop", "No new service to run", "Zero cost for the cache itself"], cons: ["Six copies of the hot set, 12 GB of replica memory", "Each replica warms alone, so hit rate drops after every deploy", "Invalidation depends on Kafka consumer lag, measured at up to 6 seconds", "A 2 GB heap raises GC pause time"], verdict: "REJECTED — fails the 2-second invalidation rule and multiplies memory by replica count", tone: rejected }
```

## Measured on the staging soak

The soak ran one hour at 4,000 rps with a price-change burst of 500 keys at the 30-minute mark. The hit-rate row reads the two minutes after that burst, which is where the options separate most.

```benchmark
id: soak
title: One-hour soak, 4,000 rps, six replicas
metricLabel: Metric
subjects:
  - { label: Redis, sub: "managed, 2 nodes", featured: true }
  - { label: Memcached, sub: "managed, 2 nodes" }
  - { label: In-process LRU, sub: per replica, tone: muted }
rows:
  - { label: Hit latency p50, better: low, cells: ["0.4 ms", "0.3 ms", "0.002 ms"] }
  - { label: Hit latency p99, better: low, cells: ["1.8 ms", "1.4 ms", "0.01 ms"] }
  - { label: Hit rate, sub: 2 min after price burst, better: high, cells: ["94%", "91%", "62%"] }
  - { label: Invalidation lag p99, sub: price change to last replica, better: low, cells: ["90 ms", "140 ms", "6.1 s"] }
  - { label: Memory for the hot set, sub: all replicas combined, better: low, cells: ["2.6 GB", "2.2 GB", "12 GB"] }
  - { label: Hit rate after a rolling deploy, sub: first 5 min, better: high, cells: ["93%", "90%", "41%"] }
  - { label: Added monthly cost, better: low, cells: ["$310", "$240", "$180"] }
note: LRU cost is the replica memory increase from 1 GB to 3 GB per pod, not a cache service.
```

The LRU wins every latency row by three orders of magnitude and loses every row that involves more than one replica. Its 6.1 s invalidation lag alone rules it out under the 2-second rule. Redis and Memcached sit within 0.1 ms of each other on hits; Redis costs $70 more per month and buys pub/sub invalidation and persistence.

## Where each option sits

```quadrant
title: Operational cost against cross-replica consistency
xAxis: { label: Operational cost, low: Nothing new to run, high: New service to page on }
yAxis: { label: Cross-replica consistency, low: Each replica drifts, high: One value everywhere }
items:
  - { x: 0.75, y: 0.9, label: Redis }
  - { x: 0.7, y: 0.75, label: Memcached }
  - { x: 0.15, y: 0.2, label: In-process LRU }
```

The top-right holds both network caches; the price of consistency is a service to operate. The LRU sits alone in the bottom-left, cheap and inconsistent. Nothing lands in the top-left, which is the quadrant a single-replica service would enjoy and this one cannot.

## Decision

```callout
tone: success
title: Redis, cache-aside, 5-minute TTL
body: "Adopt the managed Redis cluster for product-detail reads. Invalidate by publishing the product id on a Redis channel when a price changes; each replica subscribes and deletes the key. Revisit Memcached only if Redis command throughput passes 80k ops/s per node. Do not add an in-process LRU in front of Redis until a measured need appears, because the deploy-time hit-rate drop returns with it."
```