Files
OmniRoute/docs/ops/REDIS_PRODUCTION_CONFIG.md
Reza Rezaei 7360ca4242 feat(redis): add configurable key namespace prefix (#11042)
Validated on the combined board over tip 80d931ae: quota-redis-store (incl. the KEY_PREFIX derivation test), local-redis-status and rate-limiter-redis-optional green, typecheck:core clean. One pre-merge fix pushed to the branch: docs/reference/ENVIRONMENT.md gained the REDIS_KEY_PREFIX row (env-doc-sync gate requires every .env.example var documented). Board note: the redis tests leave an ioredis retry handle open and hang the runner exit locally — assertions all pass; pre-existing pattern, not from this PR. Thank you @MeRezaRezaei!
2026-08-22 18:56:45 -03:00

205 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Redis Production Configuration Guide"
version: 3.8.50
lastUpdated: 2026-08-06
---
# Redis Production Configuration Guide
## Overview
Redis is an **optional, soft dependency** in OmniRoute — the application degrades gracefully (in-memory
fallbacks) when Redis is unavailable. In production, tuning Redis reduces latency for three distinct
workloads:
| Workload | Driver | Client Factory | Key Pattern |
|---|---|---|---|
| Rate limiting | `rateLimiter.ts` | `getRedisClient()` — lazy `ioredis` singleton | `<prefix>rl:*` Luaatomic rate limit windows |
| Auth cache | `apiKeys.ts` | Reuses `rateLimiter`'s client | `<prefix>auth:api_key:<sha256>` with TTL |
| Quota store | `redisQuotaStore.ts` | Separate `getRedisClient(url)` singleton | `<prefix>quota:*` configurable per-instance |
All three workloads share one namespace prefix so OmniRoute can co-exist with other apps on a
single Redis instance (e.g. `127.0.0.1:6379`). See [Key Namespacing](#key-namespacing).
---
## Current Configuration (Code Defaults)
| Setting | Value | Where |
|---|---|---|
| `REDIS_URL` env var | `redis://redis:6379` (compose), optional | `rateLimiter.ts:5`, `.env.example` |
| `REDIS_KEY_PREFIX` env var | `omniroute:` (default) | `rateLimiter.ts`, `redisQuotaStore.ts`, `.env.example` |
| `QUOTA_STORE_REDIS_URL` env var | separate, can differ from `REDIS_URL` | `quota/storeFactory.ts` |
| `QUOTA_STORE_DRIVER` | `"sqlite"` (default), `"redis"` optional | `quota/storeFactory.ts` |
| ioredis `maxRetriesPerRequest` | `3` | `rateLimiter.ts` client creation |
| `enableReadyCheck` | not set (ioredis default: `true`) | — |
| `lazyConnect` | not set (ioredis default: `false`) | — |
| `retryStrategy` | not set (ioredis default: 200ms base, exponential) | — |
| TLS / password / DB index | **not configured** | — |
| Sentinel / Cluster | **not configured** — standalone single-node only | — |
---
## Key Namespacing
OmniRoute shares a Redis instance with whatever else runs on the host. Without a namespace,
keys like `auth:api_key:<sha256>` or `rl:*` could collide with keys from other applications
using the same Redis (this instance runs Redis on `127.0.0.1:6379` alongside other services).
Set `REDIS_KEY_PREFIX` to a non-empty string to prefix **every** OmniRoute key:
```bash
# .env — all OmniRoute keys become omniroute:rl:*, omniroute:auth:*, omniroute:quota:*
REDIS_KEY_PREFIX=omniroute:
```
- **Default:** `omniroute:` (applied when `REDIS_KEY_PREFIX` is unset or blank).
- **Applied to:** rate limiter + auth cache (shared `ioredis` client via `keyPrefix`) and the
quota store (`KEY_PREFIX = "${REDIS_KEY_PREFIX}quota"`).
- **Changing the prefix** when keys already exist in Redis orphans the old keys (they expire
via TTL / LRU). Safe to change; no migration needed.
- **ioredis `keyPrefix`** automatically prepends the prefix on writes **and** strips it on reads,
so application code never sees the prefix.
---
## Recommended Production Tuning
### 1. Connection Pool / Client Options (ioredis `Redis` constructor)
The current code creates a single `new Redis(url)` with no custom options. For production
multireplica deployments, pass a client factory in the code or wrap `getRedisClient()`:
```typescript
const redis = new Redis(REDIS_URL, {
maxRetriesPerRequest: null, // no retry limit; let retryStrategy decide
enableReadyCheck: true, // verify server is ready before accepting calls
lazyConnect: true, // don't connect on construction; wait for first call
retryStrategy: (times) => {
if (times > 10) return null; // give up after 10 retries → reconnect later
return Math.min(times * 200, 5000); // 200ms, 400ms, …, 5s cap
},
enableAutoPipelining: true, // coalesce concurrent commands into one TCP write
keepAlive: 10000, // TCP keepalive every 10s
});
```
**Key trade-offs:**
- `maxRetriesPerRequest: null` + `retryStrategy` — preferred for production so transient
Redis restarts don't immediately fail every request. The in-memory fallback in
`checkRateLimit()` absorbs the failure path.
- `lazyConnect: true` — avoids a startup dependency on Redis being up before the server
begins accepting connections.
- `enableAutoPipelining: true` — reduces round-trips for concurrent rate-limit checks;
beneficial at >50 RPS on a single connection.
### 2. Redis Server Configuration (`redis.conf`)
```
# Memory
maxmemory 80% # leave room for OS page cache
maxmemory-policy allkeys-lru # evict stale auth cache entries under pressure
# Persistence (optional — OmniRoute is crashsafe without it)
save 300 1 # snapshot at least every 5 min if ≥1 key changed
appendonly no # AOF not needed; data is regeneratable
appendfsync no # no fsync overhead (RDB is sufficient)
# Networking
timeout 0 # no idle disconnect
tcp-keepalive 300 # 5 min keepalive
tcp-backlog 511 # connection backlog for bursty load
# Performance
hz 10 # default; 100 for latencysensitive
activedefrag yes # autodefragment when fragmentation >10%
```
**Trade-off for `maxmemory-policy allkeys-lru`:** Auth cache entries may be evicted under
memory pressure. This is safe — `setCachedApiKey` always re-populates on miss, and the
SQLite fallback is authoritative. The rate-limiter Lua script creates small keys that are
short-lived by design.
### 3. Docker Compose Settings
The prod compose (`docker-compose.prod.yml`) uses `redis:8.6.2-alpine`. Add:
```yaml
redis:
image: redis:8.6.2-alpine
command: [
"redis-server",
"--maxmemory", "512mb",
"--maxmemory-policy", "allkeys-lru",
"--activedefrag", "yes",
"--save", "300 1",
]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
```
### 4. MultiInstance / Scaling Considerations
**Single Redis for all replicas** — the rate-limiter Lua script depends on a single
authoritative key space. Multiple Redis instances behind replicas would lose atomicity
and double the budget. Use a single Redis (or Redis Sentinel cluster with failover) for
all application replicas.
**Connection count:** Each application replica opens **2 TCP connections** to Redis
(rate limiter client + quota store client). At 10 replicas → 20 connections, well
within a default Redis instance's 10k connection ceiling.
### 5. Monitoring
Expose via health-check endpoint:
```typescript
// src/app/api/monitoring/health/route.ts already calls rateLimiter functions
// Add Redis-specific checks:
// 1. PING latency via ioredis .ping()
// 2. Memory usage via INFO memory
// 3. Connection count via INFO clients
// 4. Hit rate for maxmemory-policy (evicted_keys / keyspace_hits)
```
Key metrics to watch:
- **Evicted keys / sec** — if persistently non-zero, increase `maxmemory`
- **Blocked clients** — non-zero suggests slow Lua scripts or high contention
- **Rejected connections** — connection limit hit; rare at 20 connections
---
## Architecture Diagram
```mermaid
flowchart LR
subgraph App["App Replica"]
RL[rateLimiter.ts]
AK[apiKeys.ts]
QS[redisQuotaStore.ts]
end
RL -- "REDIS_URL" --> R1[(Redis\nshared)]
AK -- "reuses RL's client" --> R1
QS -- "QUOTA_STORE_REDIS_URL" --> R2[(Redis\nquota store)]
R1 --> R2 -- "can be same instance" --> R1
```
---
## References
| File | Purpose |
|---|---|
| `src/shared/utils/rateLimiter.ts` | Primary Redis client, Lua rate-limit script, in-memory fallback |
| `src/lib/db/apiKeys.ts` | Auth cache — Redis→SQLite fallback |
| `src/lib/quota/redisQuotaStore.ts` | Separate Redis client for optional quota store |
| `src/lib/quota/storeFactory.ts` | Switches between `sqlite` and `redis` quota drivers |
| `docker-compose.prod.yml` | Prod Redis container (image `redis:8.6.2-alpine`) |
| `.env.example` | Redis env vars documentation |
| `src/app/api/local/redis/` | API routes for dev container orchestration |
| `bin/cli/commands/redis.mjs` | CLI commands for dev container orchestration |