Redis¶
Redis is the backend's general-purpose hot store. It handles work that Postgres is too slow for, work that must coordinate across instances, and ephemeral state that doesn't deserve a table.
Source files referenced on this page:
backend/internal/services/redis_service.go— theRedisServiceinterface and implementationbackend/internal/middleware/rate_limiter.go,ip_blocker.go— security middlewarebackend/internal/scheduler/— distributed cron over Redisbackend/internal/worker/server.go— Asynq client/server using Redis as broker
A single Redis database (DB 0) holds everything. Keys are namespaced by feature.
Connection¶
There are two Redis clients in the process:
- The shared
*redis.Clientprovided by Wire (wiring.ProvideRedisClient) — used by middleware, the scheduler, and any service that needs raw Redis access via theredis.Cmdableinterface. - A second client owned by
RedisService(services.NewRedisService) — wraps the same Redis instance with typed helpers and the rate-limit Lua script.
Both connect to RedisConfig.Address() with the configured password. There's no Sentinel or Cluster wiring today — production runs against a single managed Redis instance.
Use cases¶
Cache¶
Generic key/value cache with TTL via RedisService.SetCache / GetCache. Used for:
- Chat message history snippets — recent N messages per room are pushed onto a Redis list and trimmed (
LPush+LTrim+Expire) - Profile snippets — discovery cards and short-lived denormalised reads
- Geocoding results — Photon / Google Places responses cached for popular queries
- Link previews — full OG metadata cached by SHA-256 of the URL (
link-preview:<hash>)
Rate limiting¶
Per-route, per-IP rate limits run a Lua script in RedisService.RateLimit:
local current = redis.call("INCR", KEYS[1])
if tonumber(current) == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
Counters use route-specific key prefixes (api_ip:<ip> baseline, register_ip:<ip>, login_ip:<ip>, reset_password_ip:<ip>). See API → Rate Limiting and Security → Rate Limiting.
IP blocking¶
Two layers backed by Redis strings:
blocked_ip:<ip>— sticky block with a TTL (set by suspicious-activity middleware or the auto-blocker on repeated rate-limit violations)violations:<ip>— rolling violation counter feeding the auto-blocker
Presence heartbeats¶
The presence service writes a TTL'd key per user every time the client heartbeats. Friends' clients read these to display online state. Active location-sharing sessions are stored as separate keys with their own TTL.
Device registration limits¶
register_device:<fingerprint> counts how many accounts have been created from one device fingerprint in a rolling 30-day window. See Security → Device Fingerprint.
Refresh token lookup¶
Refresh tokens are stored in Postgres for durability, but a parallel Redis lookup keeps the POST /api/v1/auth/refresh hot path off the DB.
Asynq task queue¶
Asynq uses Redis as its broker. Queues:
| Queue | Weight |
|---|---|
critical |
6 |
default |
3 |
low |
1 |
The Asynq server runs on every backend instance and consumes from all three. See Workers & Scheduler.
Distributed scheduler¶
The home-grown scheduler (separate from Asynq) uses Redis for:
tasks:scheduled— ZSET of pending tasks, score = epoch timestamp, member = task IDtasks:payload— HASH of task payloadstasks:queue:default— LIST of due tasks awaiting executiontasks:processing— LIST of in-flight tasks (BRPOPLPUSH-managed)tasks:active:<id>— STRING heartbeat lock for the reclaimerscheduler:leader— leader-election lock (10s TTL)
This is not Asynq; it's our own engine that powers periodic jobs (status updates, account cleanups, message expiry). See Workers & Scheduler.
Pub/Sub¶
RedisService.Publish is the only Pub/Sub touchpoint — used sparingly to broadcast events across backend replicas where the WebSocket Hub can't reach (multi-instance broadcast is not yet implemented; see the WebSocket Hub page for the single-process caveat).
Key reference¶
The canonical list of Redis keys lives at Reference → Redis Keys. Every new feature that touches Redis should add its keys there with the schema, TTL, and owning service.
Failure mode¶
RedisService.RateLimit and the rate-limit middleware fail open — if Redis returns an error, the request is allowed through. This is intentional: a Redis outage shouldn't lock everyone out of the API. Other read paths (cache misses) silently fall back to the DB. Write paths (sessions, presence) will return an error to the caller.
Operational impact of a Redis outage
A Redis outage degrades the system but does not stop it: rate limiting becomes a no-op, presence and active-location features stop updating, scheduled tasks stop firing, and the chat cache cold-misses to Postgres. Restore Redis quickly — the system is designed to tolerate brief outages, not extended ones.