Stage 7 · Master
Phase 15 — Redis Integration
Performance Testing
Quantify what Redis actually bought across caching, rate limiting, and sessions, then size and protect the instance so a cache eviction never silently deletes a live lock or session.
Three Features, Three Measured Outcomes
Redis was introduced four times in this module, each time for a stated reason. Collecting those claims in one table is not a benchmarking lesson — Performance Engineering owns benchmark methodology, statistical comparison, and load-test saturation analysis later in the course. The purpose here is narrower and specific to Redis: deciding how much memory this instance needs and what must never be evicted from it. The measurements are inputs to that sizing decision.
| Feature | Before Redis | After Redis | What actually improved |
|---|---|---|---|
| Resident profile lookup | 284 µs/op, full join every request | 6 µs/op on a warm hit | Latency and PostgreSQL connection pressure under peak concurrent reads |
| SMS budget across replicas | Limit effectively multiplied by replica count | Limit enforced exactly, regardless of replica count | Correctness of a cost control, not raw speed |
| Forced revocation on termination | Bounded only by JWT expiry; no way to reach tokens already issued | Rejected on the very next request, one key per user | A security property that did not exist before |
The SMS budget and session revocation rows are correctness and security improvements, not latency improvements — Redis earned its place in this module for three different kinds of reasons, and conflating them into a single 'is it fast' number would undersell two of the three.
A Cache Entry Disappearing Is Fine; a Lock or Session Disappearing Is Not
Redis's default eviction behavior under memory pressure — evicting keys, typically the least-recently-used ones, once maxmemory is reached — is exactly the right behavior for the resident profile cache and exactly the wrong behavior for an active lock or a revocation epoch. Losing a cached profile just means the next request re-reads PostgreSQL; losing a revocation epoch silently un-revokes a terminated staff member's access.
| Key family | Acceptable to evict under memory pressure? | Recommended handling |
|---|---|---|
| resident:*, product-style read caches | Yes — a miss just falls through to PostgreSQL | Share the default allkeys-lru database |
| revoked-before:*, denylist:* | No — silent eviction reopens the revocation gap | Separate logical database (SELECT 1) with maxmemory-policy noeviction |
| lock:* | No — an evicted lock could let two processes believe they hold it simultaneously | Same noeviction database as sessions |
| sms:budget:* | Tolerable but undesirable — eviction just resets the counter early | Same noeviction database; low absolute memory cost either way |
maxmemory 512mb
maxmemory-policy allkeys-lru # applies to database 0 — pure caching
databases 2 # database 1 holds sessions, locks, and counters
# operators connect to database 1 with SELECT 1 and rely on application code
# never writing session/lock/counter keys into database 0, and vice versaTwo logical databases inside one Redis instance are enough to separate these concerns for now — a fully separate Redis deployment per concern is a reasonable future step if either workload's memory or throughput profile grows enough to justify the added operational surface.
The Cache Needs Monitoring Too, Not Just the Services Using It
groups:
- name: redis
rules:
- alert: RedisHitRatioLow
expr: |
rate(redis_keyspace_hits_total[15m])
/
(rate(redis_keyspace_hits_total[15m]) + rate(redis_keyspace_misses_total[15m])) < 0.7
for: 15m
labels:
severity: warning
annotations:
summary: "Redis cache hit ratio has dropped below 70%, caching may no longer be earning its cost"
- alert: RedisEvictingKeys
expr: increase(redis_evicted_keys_total[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Redis is evicting keys — verify no session or lock keys were affected"RedisEvictingKeys fires on any eviction at all, with no tolerance window, because the noeviction policy on the sessions/locks database means a real eviction there should be structurally impossible — seeing this fire at all means the database separation itself has failed, not just that memory is tight.
Confirm the Separation Actually Holds Under Load
A Closing Rubric, Not a Victory Lap
- Every use of Redis in this module names a specific, measured problem it solves — none were added speculatively.
- Every cache key is namespaced by organization_id, matching the tenant-isolation discipline used everywhere else in Meridian.
- Every write path invalidates only the keys the writing service itself owns.
- Sessions and locks live where a memory-pressure eviction cannot silently delete them.
- Every new failure mode Redis introduces — an outage, a stale cache, a lost lock — has a stated, deliberate handling decision, not a silent gap.
Applied exercise
Decide the next candidate for measurement
A teammate wants to cache the organization directory search results next, citing 'it will probably help'.
- Write the exact benchmark you would require before approving this, matching the rigor of the resident profile benchmark in the first lesson.
- Name which row of the eviction-policy table this new cache would belong to, and why.
- State which service owns invalidation for it, and what write paths must call it.
- Write the one sentence you would say to the teammate if the benchmark came back showing no meaningful improvement.
Deliverable
A written benchmark plan, an eviction-policy classification, an ownership statement, and the closing sentence.
Completion checks
- The benchmark plan specifies a concrete measurement, not just 'test if it's faster'.
- The closing sentence commits to not adding the cache if the evidence doesn't support it — consistent with this module's opening principle.
Why does infra/redis.conf separate sessions, locks, and counters into a different logical database from the resident profile cache, rather than relying on key naming alone to distinguish them?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.