Skip to content

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:

  1. The shared *redis.Client from wiring.ProvideRedisClient, bound as redis.UniversalClient. The WebSocket hubs (ws.SessionHub, ws.ClientHub) take it for cross-pod PUBLISH / PSUBSCRIBE.
  2. cache.NewCache opens 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 the cache.Cache interface.

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 + Expire in 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.