Configuring Redis for Caching Web Applications
Redis is not just a cache. It's an in-memory data structure store: strings, hashes, lists, sets, sorted sets, streams, pub/sub. Redis caching is widely used in web applications. The right structure often yields a cleaner solution than trying to replicate the same in a relational database. In practice, we have seen projects where replacing N+1 queries with a cached hash reduced response time from 2 seconds to 50 ms—a 40x improvement. On the other hand, incorrect Redis configuration (e.g., missing memory limits or wrong eviction policy) can lead to production outages. Here's how to avoid that. Proper configuration can save up to 70% on infrastructure costs, potentially saving $2,000 per month for a medium-sized application.
Why Redis Is Faster Than Classic Database Caching
Caching in a relational database (e.g., MySQL MEMORY table) has limitations on volume, speed, and flexibility of data structures. Redis stores data in RAM with direct access, supports atomic operations and complex structures (sorted sets for leaderboards, streams for queues). In our tests, Redis handles 100–200 thousand read/write operations per second on a single core—10–20 times faster than database caching. This is achieved due to its single-threaded model and optimization for RAM. In one project, we switched from MySQL caching to Redis and reduced database load by 80%, saving approximately $2,000 per month in database costs. Redis performance optimization can achieve up to 500,000 operations per second on modern hardware. As noted in the Redis documentation: "Redis is an open source, in-memory data structure store, used as a database, cache, and message broker." Redis documentation
Choosing a Cache Invalidation Strategy
Cache-aside is the most common pattern: the application first checks the cache, on a miss loads from DB and stores in cache with TTL. Ideal for infrequently updated data. Write-through—synchronous write to cache and DB. Suitable for data with high update frequency (e.g., user cart).
Implement cache-aside in TypeScript with ioredis:
import { Redis } from 'ioredis'
export class CacheService {
constructor(private redis: Redis) {}
async getOrSet<T>(
key: string,
ttlSeconds: number,
factory: () => Promise<T>
): Promise<T> {
const cached = await this.redis.get(key)
if (cached !== null) {
return JSON.parse(cached) as T
}
const value = await factory()
await this.redis.setex(key, ttlSeconds, JSON.stringify(value))
return value
}
async invalidate(pattern: string): Promise<void> {
let cursor = '0'
do {
const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100)
cursor = nextCursor
if (keys.length > 0) {
await this.redis.unlink(...keys)
}
} while (cursor !== '0')
}
}
const product = await cache.getOrSet(
`product:${id}`,
300,
() => db.products.findOne({ id })
)
await cache.invalidate(`product:${id}`)
Write-through—synchronous write to cache and DB:
async updateProduct(id: string, data: UpdateProductDto) {
const updated = await db.products.update(id, data)
await redis.setex(`product:${id}`, 600, JSON.stringify(updated))
return updated
}
Comparison of cache-aside and write-through
| Parameter | Cache-aside | Write-through |
|---|---|---|
| Write latency | Low (only DB written) | Higher (wait for cache write) |
| Stale data risk | High (if not invalidated on update) | Low (consistency) |
| Implementation complexity | Low | Medium |
| DB load | Higher on misses | Lower (cache always fresh) |
How to Configure Redis for Caching: Step-by-Step
- Install Redis via package manager:
sudo apt install redis. - Set
maxmemoryand eviction policy, e.g.,allkeys-lru. We recommend at least 1GB of RAM for caching, but actual size depends on your dataset. - Enable persistence: AOF with
fsync everysec. - Implement caching pattern (cache-aside or write-through).
- Set up monitoring:
slowlog,info stats,--latency-history. - For high availability, configure Redis Sentinel or Cluster.
- Test under load and optimize TTL.
Redis Sessions
Storing sessions in Redis is standard for high-load applications. Redis sessions are stored as hashes with automatic TTL. Use sliding window with automatic TTL extension:
const SESSION_TTL = 86400 * 7
export class SessionService {
constructor(private redis: Redis) {}
async create(userId: string, metadata: SessionMeta): Promise<string> {
const sessionId = crypto.randomUUID()
const key = `session:${sessionId}`
await this.redis.hset(key, {
userId,
createdAt: Date.now(),
ip: metadata.ip,
userAgent: metadata.userAgent
})
await this.redis.expire(key, SESSION_TTL)
await this.redis.zadd(`user:${userId}:sessions`, Date.now(), sessionId)
await this.redis.expire(`user:${userId}:sessions`, SESSION_TTL)
return sessionId
}
async get(sessionId: string): Promise<SessionData | null> {
const data = await this.redis.hgetall(`session:${sessionId}`)
if (!Object.keys(data).length) return null
await this.redis.expire(`session:${sessionId}`, SESSION_TTL)
return data as SessionData
}
async destroyAll(userId: string): Promise<void> {
const sessionIds = await this.redis.zrange(`user:${userId}:sessions`, 0, -1)
if (sessionIds.length) {
const keys = sessionIds.map(id => `session:${id}`)
await this.redis.unlink(...keys, `user:${userId}:sessions`)
}
}
}
Rate Limiting via Sliding Window
Redis rate limiting is a common pattern using sorted sets. For example, we can limit each user to 100 requests per minute:
async function rateLimit(
redis: Redis,
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const now = Date.now()
const windowStart = now - windowMs
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, '-inf', windowStart)
pipeline.zadd(key, now, `${now}-${Math.random()}`)
pipeline.zcard(key)
pipeline.pexpire(key, windowMs)
const results = await pipeline.exec()
const count = results![2][1] as number
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: now + windowMs
}
}
Handling Full Memory in Redis
When memory is full, Redis evicts keys according to the maxmemory-policy. By default, some versions use noeviction, which causes write errors. We recommend allkeys-lru—evicts the least recently used keys. Also set maxmemory to 70-80% of available RAM, leaving headroom for the OS.
What's Included in Full Redis Setup
- Audit of current infrastructure and load
- Installation and configuration of Redis with maxmemory, persistence, AOF
- Implementation of caching patterns (cache-aside, write-through)
- Integration of Redis sessions and Redis rate limiting
- Redis Sentinel or Cluster setup (if needed)
- Operations documentation including cache invalidation
- Access transfer and team training
- 2 weeks of post-setup support
The cost varies depending on complexity; we provide an estimate after analysis. Our full Redis setup starts at $1,500 and includes configuration, patterns, and load testing. For a typical mid-sized web app, Redis caching can save $2,000–$5,000 monthly. Over 50 projects, we have achieved an average 60% reduction in database load.
| Work Stage | Duration |
|---|---|
| Audit and analysis | from 1 day |
| Installation and config | 1–2 days |
| Pattern integration | 2–3 days |
| Sessions and rate limiting | 1–2 days |
| Sentinel / Cluster | 1–3 days |
| Testing and documentation | 1–2 days |
Our Experience and Guarantees
We have years of production experience with Redis and over 50 projects where we set up web application caching for high-load applications. We guarantee stable operation—all configurations undergo load testing. If Redis does not deliver the expected performance improvement, we reconfigure at no extra cost. We guarantee a reduction in response times by at least 50% or a full refund.
Contact us to discuss your project. Order a Redis setup and get a consultation from an engineer with production experience.







