Redis¶
Redis is the backend's general-purpose hot store. It handles work Postgres is too slow for, work that must coordinate across instances, and ephemeral state that doesn't deserve a table.
A single Redis database (DB 0) holds everything. Keys are namespaced by feature.
Source files:
| File | Role |
|---|---|
backend/internal/platform/cache/cache.go |
The cache.Cache interface + redisCache implementation (typed helpers, geo, rate-limit Lua) |
backend/internal/wiring/providers.go |
ProvideRedisClient (shared *redis.Client), bound as redis.UniversalClient |
backend/internal/middleware/rate_limiter.go, ip_blocker.go |
Security middleware, backed by cache.Cache |
backend/internal/platform/ws/ |
Cross-pod pub/sub over redis.UniversalClient |
backend/internal/async/ |
Asynq broker (Redis-backed task queue + cron) |
Connections¶
There are two Redis client paths in the process, both pointed at RedisConfig.Address() with the configured password:
- The shared
*redis.Clientfromwiring.ProvideRedisClient, bound asredis.UniversalClient. The WebSocket hubs (ws.SessionHub,ws.ClientHub) take it for cross-podPUBLISH/PSUBSCRIBE. cache.NewCacheopens its own client wrapping the same instance with the typed helpers and the rate-limit Lua script. Every domain store, the middleware, and any service that needs Redis takes thecache.Cacheinterface.
Asynq (client, server, scheduler) opens its own connection from the same config via asynq.RedisClientOpt. There is no Sentinel or Cluster wiring; production runs against a single managed Redis instance.
Use cases¶
Cache¶
Key/value cache with TTL via cache.Cache.SetCache / GetCache, plus list and hash helpers. Used for:
- Chat message snippets — recent messages per room pushed onto a Redis list and trimmed (
CacheList=LPush+LTrim+Expirein one pipeline) - Profile / discovery snippets — short-lived denormalised reads
- Geocoding results — geocoder responses cached for popular queries
- Link previews — full OG metadata cached by SHA-256 of the URL
Event geo index¶
AddEventLocation / GetNearbyEventIDs maintain a Redis GEO set (events:geo) so the map / radar surface can resolve nearby event IDs without hitting PostGIS. The cron:redis_sync job re-syncs it from Postgres every 5 minutes.
Rate limiting¶
Per-route, per-IP limits run a Lua script in cache.Cache.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 Security → Rate Limiting.
IP blocking¶
Two layers of Redis strings, both via middleware.IPBlocker:
blocked_ip:<ip>— sticky block with a TTL (set by suspicious-activity middleware or the auto-blocker on repeated violations)violations:<ip>— rolling violation counter feeding the auto-blocker
Work locks¶
SetNX backs short-lived dedup locks, e.g. the per-item enrich lock so a double-fired task can't double-spend the upstream quota.
Presence heartbeats¶
The presence service writes a TTL'd key per user on each heartbeat; friends' clients read these for online state. Active location-sharing sessions are separate keys with their own TTL.
Device registration limits¶
register_device:<fingerprint> counts accounts created from one device fingerprint in a rolling window. See Security → Device Fingerprint.
Refresh token lookup¶
Refresh tokens are durable in Postgres, 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, both the worker queues and the cron scheduler. Queues:
| Queue | Weight |
|---|---|
critical |
6 |
default |
3 |
low |
1 |
Cross-replica cron dedup rides on Asynq's Unique lock rather than any home-grown key. See Async.
Cross-pod pub/sub¶
The WebSocket hubs fan messages across pods over Redis pub/sub: session:{id} for SessionHub (chat), client:{user_id} for ClientHub (per-user events). cache.Cache.Publish is available to services that need a raw broadcast. See WebSocket Hub.
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 schema, TTL, and owning domain.
Failure mode¶
cache.Cache.RateLimit and the rate-limit middleware fail open: if Redis errors, the request is allowed through, so a Redis outage doesn't lock everyone out. Read paths (cache misses) fall back to the DB. Write paths (sessions, presence) return an error to the caller.
Operational impact of a Redis outage
An outage degrades but does not stop the system: rate limiting becomes a no-op, presence and active-location stop updating, cron tasks stop firing, cross-pod chat fanout stalls, and the chat cache cold-misses to Postgres. The system tolerates brief outages, not extended ones.