Skip to content
Zever
Home

Redis

Switch cache, queue, session, lock, and rate limit services from memory to Redis, with no code changes.

3 min readEdit on GitHub

Most stateful services default to an in-memory adapter with a Redis twin: develop with no server, then switch configuration (not code) in production. Use Redis for any service two or more instances must share.

Point each shared service at the same addr. cache, queue, and lock read flat keys; session, ratelimit, eventbus, and idempotency nest a redis block.

Terminal window
OPTIONS_ADDR=redis:6379
OPTIONS_POOL_SIZE=20
OPTIONS_MIN_IDLE_CONNS=2
QUEUE_ADAPTER=redis
QUEUE_ADDR=redis:6379
LOCK_ADAPTER=redis
LOCK_ADDR=redis:6379
OPTIONS_REDIS_ADDR=redis:6379
Terminal window
docker run -p 6379:6379 redis:7
CACHE_ADAPTER=redis QUEUE_ADAPTER=redis go run ./cmd/myapp
server listening on :8080 (cache=redis queue=redis)

Env keys follow <SERVICE>_<FIELD>, matching ignores case and _, and on the flat services a url wins over addr. session, ratelimit, eventbus, and idempotency reject a malformed redis.addr in Options.Validate(); cache, queue, and lock ping Redis (3s limit) when first resolved, so a bad address fails on the first accessor call. Keep memory in tests: no Docker, and it passes goleak.VerifyTestMain. See each service in the config reference.

main.go
import (
"github.com/zenta-dev/zever/config"
"github.com/zenta-dev/zever/container"
)
c := container.New(config.Default())
defer c.Close(ctx)
cache, err := c.Cache()
q, err := c.Queue()
locker, err := c.Lock()
sess, err := c.Session()
limiter, err := c.RateLimit()

Register each adapter first (for example cacheredis.Register() from adapters/cache/redis), and check err after each call; accessors resolve lazily, cache, and retry after an error. c.Job() dispatches over the resolved Queue, and c.Scheduler() shares it.

Switch per service; one service on Redis never drags the rest along.

Service Memory adapter Redis role
cache memory (LRU) Shared value store through typed codecs
queue memory Topic messaging plus due and stale sweeps
session memory Server-side session rows
ratelimit memory (token bucket) Distributed counters (Lua)
lock memory Distributed leases (default ttl 30s)
idempotency memory Reserve-then-complete store
eventbus memory (fan-out) Cross-process publish/subscribe

One instance may stay on memory; with two or more, use Redis for session, lock, ratelimit, and queue, or users stick to one instance, leases split, counters diverge, and jobs duplicate.

  • Each adapter New owns its client; Close closes only that one, and c.Close(ctx) dedups shared instances by pointer.
  • queue/redis sweeps due and stale claims (promoteDue, reclaimStale) at most every 250ms, adding up to 250ms recovery latency.
  • ratelimit denials return 429 JSON with a ceiled-seconds Retry-After; limiter errors fail open; keys must be non-empty, bounded, control-byte-free, and never echoed in errors.
  • eventbus keeps the memory envelope (id, payload, headers); SubscribeChan buffers each subscriber, and slow consumers drop the newest message.
  • cache/redis and session/redis share typed codecs, so keys and TTLs match the memory twins; Redis leases are distributed, memory locks process-local.
  • Size pool_size and min_idle_conns for peak concurrency, and pass request contexts into cache, queue, and lock calls so Redis round trips observe deadlines.
  • Open without the container via cache.Open, queue.Open, or lock.Open; Options.Validate() on the nested-redis services rejects negative TTLs and oversize prefixes.
Two instances serve different sessions

Cause: both run memory sessions. Fix: point both at the same Redis addr.

Rate limits diverge across replicas

Cause: memory token buckets are per process. Fix: set ratelimit.adapter: redis.

Jobs run twice after a crash

Cause: the Redis sweep reclaims stale claims within ~250ms. Fix: make handlers idempotent, keyed by job ID.

Startup rejects the address

Cause: session, ratelimit, eventbus, or idempotency found a malformed redis.addr (a scheme or path), or cache, queue, or lock could not ping Redis. Fix: use host:port; on the flat services url wins over addr.

Shutdown hangs on Redis clients

Cause: the parent context has no deadline and a client blocks (each service then gets a 5s bound). Fix: pass a timeout context to c.Close(ctx); overruns report CloseTimeoutError, panics ClosePanicError, and failures join with errors.Join.