When developing a mobile app with a million users, the bottleneck became the news feed API endpoint: the PostgreSQL query took 300–500 ms, and at peak load the database server choked on 10,000 requests per second. We implemented Redis caching using the Cache-Aside pattern, and response time dropped to 2–5 ms, while database load decreased by 70%. Such optimization significantly reduces server infrastructure costs — typically by 40% — especially for high-load projects. Redis caching is up to 100x faster than direct database queries. Our engineers have 7+ years of experience with Redis in production and have implemented data caching for over 15 large projects with audiences of 1 million users. This article focuses on mobile backend caching and API optimization.
What Problems We Solve
The most common problem is the thundering herd: when a TTL expires, hundreds of parallel requests simultaneously hit the database. The second typical scenario is a low hit rate (below 80%), where the cache is barely used due to incorrect TTL strategy or data selection. The third problem is uncontrolled memory growth: without maxmemory, Redis can consume all RAM and crash. Our Redis configuration includes maxmemory 512mb and the allkeys-lru policy to prevent this.
We solve these with:
-
Thundering herd protection: distributed locking via
SET NXor probabilistic early expiration. - Proper TTL selection: matched to data change frequency (table below).
-
Memory limit:
maxmemory 512mb,allkeys-lrupolicy.
Our Approach: News Feed Case
For an app with 5 million users, we configured Cache-Aside with key versioning. The key is formed as feed:{user_id}:{page}:{version}, where the version changes when data changes (e.g., when a new post is added). This avoided mass invalidation. To protect against the thundering herd, we used locking:
import redis import json r = redis.Redis() def get_news_feed(user_id: int, page: int, version: int): cache_key = f"feed:{user_id}:{page}:{version}" cached = r.get(cache_key) if cached: return json.loads(cached) # Attempt to acquire lock lock_key = f"lock:{cache_key}" if r.setnx(lock_key, 1): r.expire(lock_key, 5) # 5 seconds to generate data = db.query_feed(user_id, page) r.setex(cache_key, 300, json.dumps(data)) r.delete(lock_key) return data else: # Wait and re-read from cache import time time.sleep(0.1) return get_news_feed(user_id, page, version) After implementation, the hit rate rose from 60% to 95%, and the number of evicted_keys dropped to zero. Hit rate metrics are crucial for evaluating caching effectiveness.
Why Redis Cache Invalidation Is Critical
When data is updated directly in the DB, the cache remains stale until TTL expiration. This leads to users seeing outdated information. We use event-driven invalidation: when writing to the DB, a message is sent (via Kafka or Redis Pub/Sub) that deletes the corresponding keys. For mass invalidation, we use SCAN with a cursor — this is safe for production unlike KEYS *. A more advanced approach is tagged caching: each object gets a tag stored in a SET. When the object changes, we delete all keys by the tag. This provides atomicity and controlled invalidation.
More About Thundering Herd
The probabilistic early expiration pattern is another method: some time before TTL expires (e.g., at 10% of TTL), with a 10% probability we regenerate the cache. This reduces the chance of simultaneous misses. Redis documentation recommends combining locking and early expiration for maximum reliability.
Comparison of Caching Patterns
| Pattern | Read Speed | Consistency | Implementation Complexity |
|---|---|---|---|
| Cache-Aside | High | Low (possible stale write) | Low |
| Write-Through | Medium | High (always fresh data) | Medium |
| Read-Through | High | Medium | High (requires Redis modules) |
How to Choose the Right TTL for Cache
TTL strategy is selected based on data change frequency:
| Data Type | Recommended TTL |
|---|---|
| Application configuration | 1–24 hours |
| Product catalog | 5–30 minutes |
| News feed | 1–5 minutes |
| User profile | 5–15 minutes |
Without a TTL, the cache grows until memory is full. With the allkeys-lru policy, Redis starts evicting keys when memory is full — important data may be lost unexpectedly. An explicit TTL is more reliable.
Why You Should Not Use KEYS in Production
The KEYS * command blocks Redis for the duration (O(N)), which is unacceptable under high load. Instead, use SCAN with a cursor and DEL each found key. Or tags: assign a tag to each key in a set SADD tag:user:42 "feed:42:page:1". On invalidation — SSCAN the tag and delete entries. Wikipedia about Redis doesn't provide such details, but practice confirms it.
What's Included in the Work
- Audit of current caching architecture.
- Designing key schema, TTL, and thundering herd protection.
- Implementation of the chosen pattern (Cache-Aside / Write-Through).
- Setting up Redis monitoring: track hit rate metrics, memory, evictions.
- Documentation and team training.
- Post-implementation support.
Timelines: basic setup with Cache-Aside for main endpoints — two to three business days. A full strategy with invalidation and monitoring — four to seven days.
Accelerate your mobile app with our turnkey Redis caching setup — our specialists will evaluate your project in one day and propose the optimal strategy. We guarantee 99.9% SLA. Get a consultation on implementation — just write to us.







