All posts
RedisAugust 3, 20269 min read

Scaling Redis for High-Traffic SaaS in 2026

How to scale Redis under sustained load: eviction policies, hot keys, AOF vs RDS persistence, and clustering explained for growing SaaS teams.

F
Fepiq Team
Fepiq

If you searched "how to scale Redis for high traffic" you're probably staring at a graph where p99 latency climbs, evictions are spiking, and memory usage keeps creeping toward maxmemory no matter how much you add. The short answer: pick an eviction policy that matches your access pattern, remove hot keys before they saturate a single shard, choose AOF or RDS persistence based on how much data loss you can tolerate on failover, and move to Redis Cluster or read replicas before a single primary becomes your bottleneck. The rest of this guide walks through each of those decisions the way we make them for client SaaS platforms handling millions of requests a day.

Redis is deceptively simple to get running and deceptively hard to run well once traffic is sustained rather than bursty. A cache that behaves perfectly at 500 requests per second can fall over at 20,000 rps for reasons that have nothing to do with your application code — memory fragmentation, a single hot key pinning one CPU core, or an AOF fsync policy that was fine in staging and catastrophic in production. This is a practical, load-tested walkthrough of the failure modes we see most often and how to design around them.

Memory management and eviction policies under pressure

Redis is single-threaded for command execution, and it stores everything in RAM. Once used memory approaches maxmemory, the eviction policy you chose months ago starts making decisions on every write, and the wrong choice quietly corrupts your cache hit ratio long before anyone gets paged.

PolicyBehaviorBest for
noevictionRejects writes once memory is fullRedis as a system of record, never as a pure cache
allkeys-lruEvicts least recently used key across the whole keyspaceGeneral-purpose cache, unknown or mixed access patterns
allkeys-lfuEvicts least frequently used keySkewed access patterns where popularity matters more than recency
volatile-lruLRU eviction, but only among keys with a TTL setMixed workloads combining permanent and cached data
volatile-ttlEvicts the key closest to expiring firstSession stores where near-expiry data is safest to drop

Three configuration details matter more than which policy you pick in the abstract. First, set maxmemory-samples to 10 in production — the default of 5 makes LRU/LFU approximations noticeably less accurate under real traffic. Second, leave 20-25% memory headroom below the instance's physical RAM; Redis needs room for replication buffers, AOF rewrite buffers, and fork() copy-on-write pages during BGSAVE, and running maxmemory right up against the ceiling turns a routine snapshot into an out-of-memory kill. Third, treat evictions as a first-class metric, not a footnote — alert when evicted_keys starts climbing on a dataset that should fit comfortably, because a cache that silently evicts the wrong keys turns a normal traffic spike into a wave of database queries.

Avoiding hot keys and latency spikes

Aggregate cluster metrics can look perfectly healthy while one shard is on fire. A single celebrity product ID, a global feature-flag key, or a leaderboard key that every request touches will pin all of its traffic to one node, because Redis Cluster shards by key, not by request volume. Everything else sits idle while that one shard saturates its CPU core and latency spikes across the board, even for keys that live elsewhere.

  • Find hot keys with redis-cli --hotkeys, MONITOR sampling in short bursts, or OBJECT FREQ when running an LFU policy
  • Split a single logical hot key into N physical keys (key:0 through key:N) and shard reads/writes across them client-side
  • Add a short-lived local (in-process) cache layer in front of Redis for the handful of keys that dominate traffic
  • Use client-side caching (RESP3 tracking) so read-heavy hot keys don't round-trip to Redis on every request
  • Stagger TTLs with random jitter so thousands of cached entries don't expire in the same millisecond and stampede the origin database

That last point deserves emphasis: cache stampedes are one of the most common self-inflicted outages we see. If ten thousand cached entries all carry a flat 60-second TTL, they all expire together, and ten thousand requests hit PostgreSQL or MySQL simultaneously. Adding ±10% jitter to every TTL, or using a probabilistic early-refresh algorithm, spreads that load back out and is a five-minute fix for what looks like a database scaling problem.

Persistence trade-offs under load: RDS vs AOF

Persistence is where teams get surprised the hardest, because it's invisible until a restart or a failover. RDS snapshots are cheap during normal operation but expensive at the moment they run: BGSAVE forks the process, and copy-on-write means a burst of write traffic during a large snapshot can spike memory usage well past what you'd budgeted. AOF (append-only file) gives you much better durability — with appendfsync everysec you lose at most one second of writes on a crash — but it adds continuous disk I/O and periodic rewrites that themselves fork the process the same way BGSAVE does.

For most production SaaS workloads we land on a hybrid: AOF enabled with appendfsync everysec for durability, combined with RDS snapshots on a longer interval for fast full restores and cross-region backup portability. If Redis is purely a cache that can be rebuilt from your primary database, disabling persistence entirely and relying on replication for availability is a legitimate and simpler choice — just be explicit about it rather than inheriting the default and finding out during an incident.

Clustering and replication for horizontal scale

A single Redis primary with one or two read replicas will comfortably carry most SaaS products well past their first few million dollars of ARR — don't reach for Redis Cluster before you need it, since it adds real operational complexity (no cross-slot multi-key operations, trickier client configuration, resharding coordination). The signal to move is a CPU-bound primary, not a memory-bound one: if a single core is saturated on command throughput while memory still has headroom, sharding across cluster nodes is the fix; if memory is the constraint, either evict more aggressively or scale up before you scale out.

When you do cluster, keep hash tags in mind (`{user:123}:cart` and `{user:123}:session` land on the same slot) so related keys stay co-located for multi-key operations, and watch slot balance after every resharding operation — an uneven slot distribution reintroduces the exact hot-shard problem clustering was supposed to solve. Read replicas, meanwhile, are almost always worth adding early: they're cheap, they isolate read-heavy analytics or reporting traffic from the primary that's serving your application, and they give you a promotable failover target.

Redis as a queue and session store at scale

Using Redis for background jobs (Laravel Horizon, BullMQ) and for session storage is extremely common, and both use cases have their own scaling wrinkle: they compete with your cache traffic for the same memory and the same single-threaded command loop. A large BLPOP-based queue backlog or a burst of session writes during a traffic spike can starve cache reads of latency budget on the same instance. For any SaaS product doing more than a few thousand jobs per minute, we recommend running queues, sessions, and cache in separate logical Redis databases or, once traffic justifies it, separate physical instances — it isolates blast radius and makes capacity planning for each workload honest instead of guessed.

The mistake we see most often isn't picking the wrong eviction policy — it's never revisiting the policy after the access pattern changes. A cache that was 90% cache-only data six months ago can become 40% session data today, and volatile-lru quietly starts behaving like noeviction on the keys that matter.
Fepiq engineering team

A practical checklist before your next traffic spike

  1. Confirm maxmemory is set explicitly, with 20-25% headroom below available RAM
  2. Match your eviction policy to your actual key mix, and set maxmemory-samples to 10
  3. Add TTL jitter to any cache key with a flat expiry shared by many entries
  4. Run redis-cli --hotkeys or enable OBJECT FREQ sampling and rerun it after every major feature launch
  5. Separate queues, sessions, and cache into distinct databases or instances once any one of them grows past a few GB
  6. Choose AOF everysec plus periodic RDS, or disable persistence deliberately if Redis is fully rebuildable
  7. Add a read replica before you need one; move to Cluster only when a single primary is CPU-bound, not memory-bound

None of this requires exotic tooling — it's disciplined defaults, applied before the traffic spike rather than during the postmortem. That's also exactly the kind of infrastructure decision that's easy to defer indefinitely inside a growing SaaS codebase, which is why teams often bring in outside AWS and Redis architecture review before a funding-driven traffic jump rather than after.

Frequently asked questions

How do I know if Redis is actually my bottleneck versus my application code?+

Check redis-cli --latency and INFO commandstats first. If command latency is low but your app still feels slow under load, the bottleneck is usually network round trips (too many small commands) or connection pool exhaustion, not Redis itself. Genuine Redis-side bottlenecks show up as rising evicted_keys, a saturated CPU core on one shard, or slowlog entries for specific commands.

What's the difference between allkeys-lru and volatile-lru in practice?+

allkeys-lru can evict any key in the dataset once memory is full, including ones without a TTL. volatile-lru only evicts keys that have an expiry set, leaving TTL-less keys untouched even under memory pressure. Use volatile-lru when Redis holds a mix of permanent data and disposable cache entries; use allkeys-lru when everything in that instance is genuinely a cache.

Does Redis Cluster fix hot key problems automatically?+

No — clustering shards by key, so a single hot key still lands entirely on one node and can saturate that shard's CPU even while the rest of the cluster is idle. Clustering helps with overall dataset size and aggregate throughput, but hot keys need to be split or cached client-side regardless of how many nodes you run.

Should a SaaS product use Redis for both caching and its job queue?+

It can, but at meaningful scale it's safer to isolate them into separate logical databases (SELECT 0-15) or separate instances. Queue workloads (BLPOP, XREAD) and cache workloads compete for the same single-threaded command loop and memory budget, and an unexpected job backlog shouldn't be able to degrade your cache hit latency.

Is AOF or RDS persistence better for a production Redis cache?+

AOF with appendfsync everysec gives you at most one second of data loss on a crash and is the safer default for anything holding sessions or queue state. RDS snapshots are lighter-weight and better for fast full restores or backups. Most production setups use both together; a pure cache that's fully rebuildable from your primary database can reasonably disable persistence entirely.

Running background jobs on Redis already? See how Laravel Horizon and BullMQ compare for queue throughput and reliability at scale.

Compare Horizon vs BullMQ

Planning for a traffic spike or a migration to Redis Cluster? Fepiq's team designs and load-tests Redis and AWS architecture for growing SaaS products.

Talk to our team

Get new posts in your inbox

Occasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.

Keep reading

Related posts

All posts

Let's build something

Ready to ship your next product with Fepiq?

Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.