Files
OmniRoute/docs/redis-production-config.md
Diego Rodrigues de Sa e Souza 04683029a6 fix(build): exec native esbuild binary directly in prepublish (dast-smoke base-red) (#9558)
* fix(build): exec native tool binaries directly in runBuildTool

#8858 routed every resolved local bin through process.execPath to avoid
Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE
platform executable (ELF on Linux), so Node parsed machine code as JS and
build:cli died with 'SyntaxError: Invalid or unexpected token', turning
dast-smoke red for every PR.

runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and
execs native binaries directly; JS entries keep going through this Node
binary (the .cmd-shim avoidance #8858 wanted).

Validation (RED->GREEN on this box):
- RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF)
- GREEN: the exact failing CI step reproduced via the new logic bundles
  open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s).

* fix(docs): add MDX frontmatter to the 20 remaining docs without it

Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS
(this run's dast-smoke red): any doc without frontmatter breaks the
fumadocs MDX loader during next build, killing build:cli/dast-smoke for
every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this
class cannot recur one file at a time.

* docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars

Pre-existing env/docs contract drift from recently merged features made
check:env-doc-sync red for any docs-touching PR. Values and defaults read
from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts).

* fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows

The opencode-plugin step hard-failed on GitHub runners because
resolveBundledNpmEntry only looked next to the node binary (Windows zip
layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm.
Added that candidate, and when neither exists on non-Windows the step now
falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is
Windows-only.

* test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles

The test landed on release/v3.8.50 covering
open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker
registration, so Fast Quality Gates' drift detection reds any PR that
carries it. Mechanical registration so its mutant kills count.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 02:19:57 -03:00

6.7 KiB
Raw Blame History

title, version, lastUpdated
title version lastUpdated
Redis Production Configuration Guide 3.8.50 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 Luaatomic rate limit windows
Auth cache apiKeys.ts Reuses rateLimiter's client auth:api_key:<sha256> with TTL
Quota store redisQuotaStore.ts Separate getRedisClient(url) singleton Configurable per-instance

Current Configuration (Code Defaults)

Setting Value Where
REDIS_URL env var redis://redis:6379 (compose), optional rateLimiter.ts:5, .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

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():

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:

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:

// 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

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