×marble
all posts
Jun 16, 2026·11 min read

Redis vs DynamoDB for Feature Serving: Which Wins at p50 < 30 ms?

Redis and DynamoDB both claim single-digit millisecond reads — but personalization feature serving has Zipfian hot keys, sub-30ms budgets, and eviction semantics that expose the real tradeoffs. Here's how to choose.

Alex Shrestha·Founder, ×marble

Redis vs DynamoDB for Feature Serving: Which Wins at p50 < 30 ms?

TL;DR.

  • Redis delivers p50 < 1 ms from the same AZ; DynamoDB delivers p50 ~2–5 ms and p99 ~8–15 ms — fine for simple lookups, not fine when you're compositing 10–50 features per inference request.
  • Hot-key amplification is the silent killer: Zipfian user-signal distributions mean 1% of users generate 30–50% of feature reads — Redis pipelining and read replicas absorb this; DynamoDB DAX does too, but at a different cost curve.
  • Eviction policy is an inference correctness concern, not an ops detail: allkeys-lfu on Redis will silently degrade day-zero personalization if your cold-start keys aren't isolated from the LFU frequency pool.
  • In 2026, Valkey and ElastiCache Serverless changed the cost-per-QPS math enough that "Redis is expensive" is no longer a given above ~3,000 RPS.
  • If you only remember one thing: your feature store choice determines whether personalization runs at model speed or at database speed — treat p99 latency as a hard constraint, not a metric to average away.

Personalization inference is a fan-out problem. One request fans out to a feature lookup for the user, lookups for each candidate item, and knowledge graph traversal to assemble context. If that fan-out touches a feature store that can't sustain sub-30ms p50 reads under Zipfian load, your latency SLA is dead before the model runs a single forward pass. Redis and DynamoDB are the two default choices at this layer — both are fast, both are managed, and both will fail you in different ways if you choose wrong for your access pattern.

What does "feature serving" actually mean at inference time?

Feature serving is the real-time retrieval of precomputed signals needed at inference. For a personalization engine, that includes: user embeddings, entity preference scores, recency-weighted interaction history, knowledge graph neighborhood summaries, and cold-start priors for new users. These are written by offline or near-real-time pipelines and read by the serving layer at request time with no tolerance for latency surprises.

The serving contract is strict. Reads must be low-latency, high-throughput, and tolerant of missing keys — a cache miss on a user feature should fall back to a synthetic clone or prior, not crash your ranking model. This fallback behavior is coupled directly to your store's eviction policy, which is why eviction is a first-class design concern rather than something you tune after launch.

Two read patterns dominate:

  • Point reads. Single GET user:{id}:embedding. Latency-dominated. Redis was designed for this.
  • Batched multi-key reads. MGET or BatchGetItem for 20–100 item features per request. Throughput- and fan-out-dominated. DynamoDB was designed for this — but the distinction breaks down under Zipfian load.

Redis vs DynamoDB: latency by the numbers

Redis is in-memory by design. A point read from a Redis node in the same Availability Zone is consistently sub-millisecond — p50 around 0.3–0.8 ms, p99 under 2 ms under normal load. DynamoDB's headline is "single-digit millisecond"; in practice p50 sits at 2–5 ms and p99 at 8–15 ms for point reads, depending on partition heat and item size. (AWS DynamoDB developer guide)

Those numbers look close until you do the fan-out math. A request that needs 50 item-feature reads:

  • Redis PIPELINE (single RTT for all 50 keys): p50 ~1–2 ms total
  • DynamoDB BatchGetItem (up to 100 keys per call): p50 ~5–12 ms, subject to hot-partition degradation

A 10 ms difference at the feature layer is a 33% budget hit if your total serving budget is 30 ms. Multiply by the number of sequential feature-fetch stages in your pipeline and the margin collapses entirely.

DynamoDB Accelerator (DAX) is DynamoDB's in-memory caching layer. It brings reads to microsecond latency for cache hits — competitive with Redis. But DAX is a provisioned cluster with its own hot-key behavior under Zipfian load, billed separately, and requires you to think about the same eviction tradeoffs that Redis surfaces more explicitly.

p99 is where the real separation happens

For personalization, p99 matters more than p50. A slow tail on feature reads means the recommendation arrives after the user has already scrolled past the fold or tapped away. Redis p99 degrades gracefully when the connection pool is properly sized and Lua scripts reduce round trips. DynamoDB p99 is more volatile because it is partition-bound: a hot partition pushes all requests into a retry loop. AWS's Adaptive Capacity absorbs gradual shifts, but not sudden spikes. The SDK's built-in RetryOnThrottlingException adds latency, not removes it.

If your target is p99 < 20 ms for the feature layer, Redis with read replicas is the safer guarantee. DynamoDB + DAX can match it if your access pattern is uniform enough — but Zipfian user distributions rarely are.

The Zipfian problem: why hot keys matter more in personalization than in most workloads

In any real user base, request frequency follows a Zipfian distribution (Wikipedia): a small fraction of users generates a disproportionate share of reads. In consumer apps, the top 1% of active users typically produce 30–50% of personalization feature lookups. Power users, automated re-ranking triggers, live-event surges — they all concentrate reads on a narrow key space at the worst possible moment.

In Redis, a single key's throughput is bounded by the CPU of the shard holding that key. Redis Cluster shards by key hash, so a hot key saturates a single node while others sit idle. The mitigations have real costs:

  • Key sharding with suffix randomization. Append a random suffix (user:{id}:emb:{0..N}) and read all N shards in a pipeline, averaging results. Distributes load but adds read complexity and a coordination step.
  • Read replicas. ElastiCache for Redis supports read replicas. Route user-feature reads to replicas, write-path updates to primary. Doubles your node count for the hot read path.
  • Server-assisted client-side caching. Redis 6+ supports CLIENT TRACKING with invalidation messages. Track invalidation on the client and serve from process memory for ultra-hot user features. Sub-microsecond for the hottest keys with Redis handling invalidation. (Redis CLIENT TRACKING docs)

DynamoDB's hot-key problem is structurally different. Partitions are auto-split by AWS based on throughput, but splitting is not instantaneous. A sudden spike on a user-feature key will hit throttling before partition split catches up. Adaptive Capacity helps, but under sustained hot traffic it has hard limits. For personalization workloads where the user distribution is inherently Zipfian, Redis with explicit hot-key mitigations typically outperforms DynamoDB + DAX on p99 tail latency when the hot set is non-trivial.

Eviction, TTL, and feature staleness: the inference correctness problem

Eviction is not a cache ops concern — it is a model correctness concern. Here is why.

In a personalization engine, user features have different freshness requirements: interaction history (last 24h events) should be refreshed every few minutes; long-term preference embeddings are stable over days; synthetic clone cold-start priors should persist longer than warm-user features, because a miss on a new user has no fallback. These namespaces should not share an eviction pool.

Redis gives you fine-grained control: allkeys-lru, allkeys-lfu, volatile-lru, volatile-lfu, volatile-ttl. The right policy for feature serving is typically volatile-lfu combined with explicit TTLs on each key — LFU naturally keeps hot user features in memory and evicts long-tail users first. But there is a subtle failure mode: new users have low access frequency by definition, so LFU will evict cold-start features before they are used, precisely when they are needed most.

The clean fix is namespace separation. Store synthetic clone features under a dedicated key prefix in a separate Redis logical database or cluster configured with volatile-ttl policy. Cold-start keys get a long TTL; warm-user features get shorter TTLs under LFU. At eviction pressure, warm-user low-frequency keys go first, cold-start keys survive until their TTL expires. You can also warm up a key's LFU counter at write time by issuing a pipeline of reads immediately after the SET — inelegant, but it works if you cannot separate namespaces.

DynamoDB does not evict — it uses TTL for item expiration, which is asynchronous and best-effort. AWS guarantees removal within 48 hours of TTL expiry, not immediately. A feature with an expired TTL that has not been garbage collected yet is still readable, which means your inference pipeline can silently serve stale predictions. The mitigation is to version your features in the key (user:{id}:emb:v{epoch}) and treat any read from a version older than your recomputation interval as a miss.

Redis eviction is synchronous and deterministic. DynamoDB TTL expiry is neither. For inference pipelines where feature freshness directly impacts recommendation quality, Redis's eviction model is easier to reason about and audit.

Cost-per-QPS: the math nobody runs before choosing

The standard framing is "Redis is expensive (memory is costly), DynamoDB is cheap (pay per request)." In 2026, this framing is outdated for high-QPS personalization workloads.

DynamoDB on-demand pricing (us-east-1):

  • Read request unit: $0.25 / million RRUs
  • At 10,000 RPS with BatchGetItem fetching 20 item features each (= 200,000 item reads/sec): ~$4,320/day in read RCUs alone
  • DAX cluster minimum (2× dax.r5.large): ~$385/month, plus you still pay RCUs for cache misses

ElastiCache for Redis pricing:

  • cache.r7g.large (2 vCPU, 13 GB): ~$0.166/hr = ~$120/month per node
  • A 2-node cluster (primary + replica) handles 10,000 RPS with headroom at ~$240/month total
  • ElastiCache Serverless (2026 pricing): per ECU-hour and GB-hour — better than provisioned for bursty workloads with headroom waste

The crossover: above roughly 3,000–5,000 RPS of feature reads, ElastiCache for Redis is cheaper than DynamoDB + DAX. Below ~1,000 RPS, DynamoDB on-demand wins because there is no cluster to pay for when idle.

Valkey (the Linux Foundation fork of Redis, maintained by AWS, Google, Snap, and others following the 2024 SSPL license change) removes the ElastiCache markup entirely for teams willing to self-host on ECS or EKS. Redis-compatible semantics, Graviton instances, infrastructure-only cost. Dragonfly is another 2026-relevant alternative: single-node Redis-compatible with multi-threaded vertical scaling, which eliminates Redis Cluster sharding complexity for hot-key distribution at the cost of a smaller ecosystem. (Dragonfly)

Decision framework: three axes, not one benchmark

The right choice is not determined by "which is faster in a benchmark" but by where your workload sits on three axes simultaneously.

Axis 1: Latency target

  • p50 < 5 ms: Redis or DAX both viable
  • p50 < 2 ms: Redis only
  • p99 < 10 ms under Zipfian load: Redis with read replicas

Axis 2: Access pattern

  • Near-uniform reads (B2B, enterprise user base, limited power users): DynamoDB + DAX is safe
  • Zipfian reads (consumer app, viral content, cold-start heavy): Redis with explicit hot-key strategy

Axis 3: Operational model

  • Small team, no Redis ops experience: DynamoDB (fully managed, zero eviction tuning required)
  • ML or infra team that needs per-namespace cache semantics: Redis (eviction policies, Lua atomics, TTL per feature type)

Choose DynamoDB when:

  • QPS is below 1,000 and bursty — pay-per-request means near-zero cost at low traffic
  • You need cross-region replication for regulatory compliance — DynamoDB Global Tables handles this natively; Redis cross-region replication is manual orchestration
  • Your team already runs DynamoDB and the operational overhead of a second store is not justified by the latency gain

Choose Redis when:

  • You are running day-zero personalization where new-user cold-start features must be served from synthetic clone priors at sub-2ms latency
  • Your inference pipeline performs online feature computation (sliding-window aggregates, real-time graph traversal results) and needs atomic read-modify-write semantics — Redis Lua scripts handle this without the ConditionExpression complexity of DynamoDB
  • You are above 3,000 RPS and total cost matters

The hybrid pattern: Redis as L1, DynamoDB as L2

The pattern that scales cleanest for personalization at the knowledge graph layer in 2026 is a two-tier feature store:

  • L1 (Redis/Valkey): hot user features, real-time interaction signals, cold-start priors for recently active users. TTL of 1–24 hours. Policy: volatile-lfu with cold-start keys in a separate namespace on volatile-ttl.
  • L2 (DynamoDB): long-term preference embeddings, historical feature snapshots, fallback for evicted keys. TTL of 7–30 days. Written asynchronously by offline batch jobs.

The serving layer reads L1 first. On miss, it reads L2, promotes the result back to L1, and continues. DynamoDB only takes traffic on cache misses — which for a properly sized Redis L1 is 5–15% of total reads. At that hit rate, DynamoDB's higher latency and per-read cost are never on the critical path.

This is the same tiered caching insight behind the original Dynamo architecture (DeCandia et al., 2007), applied to the feature serving layer. The key operational discipline: L1 TTL must be shorter than your feature recomputation interval. If user embeddings are recomputed every 6 hours by your batch pipeline, L1 TTL should be 4–5 hours — forcing a fresh read from L2 (which holds the updated embedding) before serving a stale one.

What to build first

If you are standing up a feature serving layer today:

1. Start with Redis (or Valkey) for user features. Use volatile-lfu as your eviction policy. Set explicit TTLs per feature namespace: short TTL (15–60 min) for real-time interaction signals, longer TTL (12–48 hr) for embeddings and graph features.

2. Isolate cold-start features from warm-user features. Store synthetic clone priors in a separate key prefix, ideally a separate Redis logical database with volatile-ttl policy. A new user's first feature read must not lose an eviction race against an established user's stale low-frequency key.

3. Benchmark p99 under synthetic Zipfian load before you ship. Use a key distribution that matches your expected DAU-to-power-user ratio. If you have no historical data, a Zipf exponent of 1.2 is a reasonable prior for consumer social apps (Wikipedia: Zipf's law). Verify that your p99 holds under the top-1% hot key load, not just average load.

4. Add DynamoDB as L2 when you need durability, longer retention, or cross-region coverage. Do not start with DynamoDB as your primary path. The eviction and staleness complexity is harder to manage before you understand your feature access patterns.

5. Instrument hit rate per feature namespace, not just overall. A 92% aggregate hit rate can hide a 40% miss rate on cold-start features that silently degrades new-user personalization. Track hits and misses broken out by user age bucket: new (<1 day), early (1–7 days), established (7–30 days), retained (30+ days). The new-user bucket is where the story gets told.

The choice between Redis and DynamoDB for feature serving is ultimately a choice between two failure modes: Redis fails on eviction (silent prediction degradation, recoverable with namespace tuning) and DynamoDB fails on hot partitions (tail latency spikes, recoverable with Adaptive Capacity, but noisier to debug). Know your Zipfian distribution, set your latency budget as a hard constraint, and instrument the layer before you scale it — not after.

the product behind these notes

×marble is the personalization graph.

One API. A living knowledge graph per user. Day-zero ready, explainable by construction. We built it so you don't have to.

See ×marble