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.
Assumptions
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.
Three ways to cache product-detail reads
- 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
- 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
- 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
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.
One-hour soak, 4,000 rps, six replicas
| Metric | Redismanaged, 2 nodes | Memcachedmanaged, 2 nodes | In-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
Operational cost against cross-replica consistency
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.