Skip to main content

Redis as In-Memory Database

While commonly used as a cache, Redis can serve as a primary database for workloads where speed matters more than ACID guarantees and where the dataset fits in RAM. Understanding its persistence and durability characteristics is essential for senior engineers.


Redis as Primary DB vs Cacheโ€‹

DimensionRedis as CacheRedis as Primary DB
Data loss toleranceAcceptableNot acceptable
Dataset sizeCan exceed RAM (use eviction)Must fit in RAM
PersistenceOptionalRequired (RDB + AOF)
ConsistencyEventualDepends on config
Recovery timeFast (warm cache)Depends on AOF size
Use caseSpeed layer on top of real DBSessions, leaderboards, real-time data

Persistence Mechanismsโ€‹

1. RDB (Redis Database Snapshot)โ€‹

Creates a point-in-time binary snapshot of the entire dataset via fork().

# redis.conf
save 3600 1 # Snapshot if 1+ change in last 3600s
save 300 100 # Snapshot if 100+ changes in last 300s
save 60 10000 # Snapshot if 10000+ changes in last 60s

# Manual trigger
BGSAVE # Forks Redis process, writes snapshot in background
LASTSAVE # Unix timestamp of last successful save

# Disable all saving
save "" # Pure cache mode โ€” no persistence

How RDB fork() works:

Main Redis Process (parent)
โ†“ fork()
Child Process (snapshot writer)
โ†“
Writes binary .rdb file to disk

Main process continues serving requests.
Copy-on-Write (CoW): modified pages are copied when written.
Both processes see the same memory initially.

RDB trade-offs:

ProCon
Compact single fileData loss between snapshots
Fast DB load on restartfork() can cause latency spike (large heaps)
Good for backupsNot suitable for strict durability
Minimal IO during operation10GB heap โ†’ 100โ€“200ms latency spike on fork

Monitoring RDB fork latency:

INFO latencystats
LATENCY HISTORY fork
# Or check: rdb_last_bgsave_time_sec

2. AOF (Append-Only File)โ€‹

Logs every write command to disk. On restart, Redis replays the AOF to rebuild the dataset.

# redis.conf
appendonly yes
appendfsync everysec # Recommended โ€” sync to disk every second
# appendfsync always # Sync after every write (slowest, most durable)
# appendfsync no # OS decides when to flush (fastest, least safe)

AOF fsync modes:

ModeDurabilityPerformanceRisk
alwaysMax (~0 data loss)Slow (disk write per command)Low throughput
everysec~1 second of data lossGoodRecommended
noOS-controlledFastestUp to kernel buffer loss on crash

AOF Rewrite (compaction):

# AOF grows indefinitely โ€” rewrite compresses it
BGREWRITEAOF # Background AOF rewrite

# Auto-rewrite when AOF grows
auto-aof-rewrite-percentage 100 # Rewrite when AOF doubles in size
auto-aof-rewrite-min-size 64mb # Minimum size before rewrite

During rewrite, Redis forks and writes a new AOF from current in-memory state. Parent continues appending to the old AOF; differences are merged when the child completes.

AOF trade-offs:

ProCon
Near-real-time durabilityLarger file than RDB
Can reconstruct to exact stateSlower restart (replay all commands)
Human-readable formatAOF rewrite causes another fork() spike
everysec = at most 1s data lossOld commands not compacted until rewrite

Use both for the best of both worlds:

appendonly yes
appendfsync everysec
save 3600 1
save 300 100

# On restart: Redis prefers AOF (more complete); RDB used as backup
aof-use-rdb-preamble yes # Redis 4.0+: AOF starts with RDB snapshot header โ†’ faster rewrite

Recovery scenarios:

FailureRDB onlyAOF onlyBoth
Server crashLose since last snapshotLose up to 1sLose up to 1s
Corrupted AOFN/ANeed redis-check-aof repairRDB as fallback
Disk failureLose everythingLose everythingSame

Data Modeling for Redis as Primary DBโ€‹

Use Case: User Sessionsโ€‹

# One hash per session (rich structure, partial field update)
HSET session:abc123 userId "1234" role "admin" expiresAt "1700000000"
EXPIRE session:abc123 3600

# Index by userId (for "show all sessions for user")
SADD user:1234:sessions "abc123"

Use Case: Real-Time Leaderboardโ€‹

# Sorted Set โ€” atomic, O(log N) updates
ZADD game:leaderboard 15000 "alice"
ZADD game:leaderboard 12500 "bob"
ZINCRBY game:leaderboard 500 "alice" # Atomic score increment
ZREVRANK game:leaderboard "alice" # Rank (0-indexed) โ†’ 0 (first place)
ZREVRANGEBYSCORE game:leaderboard +inf -inf WITHSCORES LIMIT 0 10 # Top 10

Use Case: Rate Limiting (Primary Store)โ€‹

-- Lua script: atomic sliding window rate limit
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window * 1000)
return 1 -- allowed
end
return 0 -- blocked

Use Case: Distributed Lock (Redlock)โ€‹

# Single-node lock (sufficient for most cases)
SET lock:resource "owner-uuid" NX EX 30
# NX: only set if not exists
# EX: auto-release after 30s (prevents deadlock if owner crashes)

# Release lock (Lua for atomicity โ€” don't release if owner changed)
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0

Redlock (multi-node): For stronger guarantees, acquire lock on N/2+1 nodes:

# Redlock algorithm (5 Redis nodes)
nodes = [redis1, redis2, redis3, redis4, redis5]
acquired = sum(1 for r in nodes
if r.set("lock:key", token, nx=True, ex=30))
if acquired >= 3: # Majority
return True # Lock acquired

Clock drift caveat: Redlock assumes clocks don't drift significantly. Martin Kleppman's critique: use fencing tokens (monotonic counter) for true linearizability.


Spring Boot: Redis as Primary Storeโ€‹

Entity Mapping with Spring Data Redisโ€‹

@RedisHash(value = "products", timeToLive = 3600)
public class Product {

@Id
private String id;

@Indexed // creates secondary index for queries
private String category;

private String name;
private BigDecimal price;
private int stock;

// getters, setters
}
public interface ProductRedisRepository extends CrudRepository<Product, String> {
List<Product> findByCategory(String category);
}
@Service
public class ProductService {

@Autowired
private ProductRedisRepository repository;

public Product save(Product product) {
if (product.getId() == null) {
product.setId(UUID.randomUUID().toString());
}
return repository.save(product);
}

public List<Product> findByCategory(String category) {
return repository.findByCategory(category);
}
}

Durability Guarantees by Configurationโ€‹

ConfigMax Data LossThroughputUse Case
No persistence100%HighestPure cache
RDB only (60s)Up to 60sHighCache with backup
AOF noOS bufferHighBalanced
AOF everysecUp to 1sMedium-HighRecommended primary
AOF always~0LowCritical data
RDB + AOF everysecUp to 1sMedium-HighProduction DB

Redis vs Traditional Databasesโ€‹

RedisPostgreSQL / MySQLMongoDB
StorageIn-memory (optional disk)Disk-firstDisk-first
Throughput100Kโ€“1M+ ops/sec1Kโ€“100K ops/sec10Kโ€“100K ops/sec
Query languageCommand-basedSQLQuery DSL
JoinsโŒ (model differently)โœ…โŒ (embed/link)
TransactionsLimited (MULTI/EXEC)Full ACIDMulti-doc (limited)
Dataset sizeLimited by RAMUnlimitedUnlimited
Full-text searchLimitedpgvector, FTSAtlas Search
Best forSpeed-critical, structured dataComplex queries, ACIDFlexible documents

When to Use Redis as Primary DBโ€‹

Good fit:

  • Sessions, tokens, temporary data with natural TTL
  • Leaderboards, rankings, counters
  • Rate limiting, quota tracking
  • Short-lived job/task queues
  • Real-time collaborative features (cursors, presence)
  • Configuration/feature flags

Bad fit:

  • Complex relational queries (joins, aggregations)
  • Large datasets that don't fit in RAM
  • Long-term audit logs (use Elasticsearch or cold storage)
  • Financial transactions requiring ACID guarantees
  • Full-text search (use Elasticsearch)