Valkey — Overview
Valkey (a Redis-compatible in-memory store) is used for chat session persistence, anonymous user identity, and request/token rate limiting. The server connects on startup and exposes its health via GET /health.
Connection
| Setting | Default | Env variable |
|---|---|---|
| URL | valkey://localhost:6379/0 | TURINGDB_VALKEY_URL |
The client is created with decode_responses=True, so all values are returned as strings rather than bytes.
Relevant files: server/config.py, server/main.py
Key namespaces
All keys follow a hierarchical, colon-separated naming convention.
| Namespace prefix | Type | TTL | Description |
|---|---|---|---|
chat:session:{session_id} | String (JSON) | 7 days | Full chat session (messages + metadata) |
chat:job:{job_id} | String (JSON) | 10 minutes | Job record (status, result, error) |
chat:job_queue | List | — | FIFO queue of pending job IDs (BLPOP/RPUSH) |
user:{user_id} | String (JSON) | none (permanent) | User record |
user:{user_id}:sessions | List | none (permanent) | Ordered list of session IDs for a user |
anon:{anon_id} | String (JSON) | 3 days | Anonymous ID → user mapping |
rate_limit:{user_id} | List | 10 minutes (rolling) | Request timestamps for sliding-window request limiting |
token_limit:{user_id} | Sorted Set | 10 minutes (rolling) | Token usage entries for sliding-window token limiting |
session_id and user_id are UUID v4 strings. anon_id is a secrets.token_urlsafe(32) token.
TTLs on rate_limit:* and token_limit:* keys are refreshed on every write. TTL on chat:session:* keys is refreshed on every write.
Identity and session lifecycle
GET /chat/bootstrap-anon
│
├─ anon_id cookie absent → generate anon_id token
│ SETEX anon:{anon_id} 259200 {user_id, created_at}
│ SET user:{user_id} {user_id, created_at}
│ Set cookie anon_id (max-age=259200, httponly=True)
│
└─ anon_id cookie present
GET anon:{anon_id}
├─ not found → treat as absent (new user)
├─ age < 2 h → return existing user_id unchanged
└─ age ≥ 2 h → rotate: delete old anon key, write new one
POST /chat/message {message, session_id?} [X-Anon-Id header]
│
├─ GET anon:{anon_id} → resolve user_id
├─ check rate_limit:{user_id} (reject 429 if exceeded)
├─ check token_limit:{user_id} (reject 429 if exceeded)
├─ GET chat:session:{session_id} (or create empty ChatSession)
├─ SET chat:job:{job_id} <JobRecord json> (TTL 10 min)
├─ RPUSH chat:job_queue {job_id}
└─ → returns 202 JobSubmission immediately
Background worker (BLPOP chat:job_queue):
├─ run intent router → emit routing events to session
├─ run lookup or agent handler
├─ SETEX chat:session:{session_id} 604800 <json>
├─ SET chat:job:{job_id} <JobRecord status=done> (TTL refresh)
└─ if new session: LPUSH user:{user_id}:sessions {session_id}
Operations reference
| Operation | Command | Where |
|---|---|---|
| Read session | GET chat:session:{id} | chat-agent/session.py:get_or_create_session |
| Write session | SETEX chat:session:{id} {ttl} {json} | chat-agent/session.py:save_session |
| Read anon mapping | GET anon:{anon_id} | server/user.py |
| Write anon mapping | SETEX anon:{anon_id} 259200 {json} | server/user.py |
| Write user | SET user:{user_id} {json} | server/user.py |
| Prepend session ID | LPUSH user:{user_id}:sessions {session_id} | server/user.py:add_session_to_user |
| List session IDs | LRANGE user:{user_id}:sessions 0 -1 | server/user.py:get_user_sessions |
| Prune expired sessions | DEL + LPUSH (surviving IDs only) | server/user.py:get_user_sessions |
| Check request rate | LRANGE + LPUSH + LTRIM + EXPIRE on rate_limit:{user_id} | server/rate_limit.py:check_rate_limit |
| Check token budget | ZREMRANGEBYSCORE + ZRANGE on token_limit:{user_id} | server/rate_limit.py:check_token_budget |
| Record token usage | ZADD + EXPIRE on token_limit:{user_id} | server/rate_limit.py:record_token_usage |
| Health check | PING | server/main.py:/health |
| Shutdown | aclose() | server/main.py:api_lifespan |
Known limitations and planned work
| Limitation | Notes |
|---|---|
secure=False on the anon_id cookie | Development only — must be True before any HTTPS deployment. |
| User records have no TTL | Permanent user keys accumulate indefinitely. Expiry or archival policy needed before production. |
| Session list not capped | user:{user_id}:sessions grows unboundedly. A LTRIM after LPUSH (e.g. keep last 100) should be added. |
| No Valkey persistence config documented | appendonly / save settings for the Valkey process itself are not yet specified. Data is lost on restart unless configured. |
CORS is ["*"] | Must be restricted to known origins before any HTTPS deployment. |