# Redis

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

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.

## Quick start

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

```sh
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
```

```sh
docker run -p 6379:6379 redis:7
CACHE_ADAPTER=redis QUEUE_ADAPTER=redis go run ./cmd/myapp
```

```text
server listening on :8080 (cache=redis queue=redis)
```

Env keys follow `_`, 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](https://zenta-dev.github.io/zever/reference/config/#cache).

## Resolve the services

```go title="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.

## Decide which services use Redis

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.

## Understand client ownership and behavior

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

## Troubleshooting

<details>
<summary>Two instances serve different sessions</summary>

Cause: both run `memory` sessions. Fix: point both at the same Redis `addr`.
</details>

<details>
<summary>Rate limits diverge across replicas</summary>

Cause: `memory` token buckets are per process. Fix: set `ratelimit.adapter: redis`.
</details>

<details>
<summary>Jobs run twice after a crash</summary>

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

<details>
<summary>Startup rejects the address</summary>

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`.
</details>

<details>
<summary>Shutdown hangs on Redis clients</summary>

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`.
</details>

## Related

- [Cache](https://zenta-dev.github.io/zever/guides/data/cache/)
- [Sessions](https://zenta-dev.github.io/zever/guides/http/sessions/)
- [Middleware](https://zenta-dev.github.io/zever/guides/http/middleware/)
- [Container](https://zenta-dev.github.io/zever/concepts/container/)
