Enterprise AI Redis Rate Limiting WebSocket Security HMAC Getraind Docker

"Redis in Enterprise AI: Rate Limiting, Token Security, and Session Management"

Mahesh Pabba 2026-06-20 All posts

Redis is one of those components where the gap between "installed" and "configured correctly" is wide and consequential. I've seen Redis deployed without a memory cap slowly consume all available RAM until the server falls over. I've seen Redis without AOF disabled creating unnecessary I/O pressure on a machine where every byte of disk bandwidth matters. I've seen Redis not deployed at all, which means WebSocket tokens can be replayed and rate limiting doesn't exist.

In Getraind, Redis handles three specific things — and it does them exactly right, or the platform is compromised.


What Redis Does in This Platform

Redis is not a general-purpose cache here. That role belongs to MongoDB (ExactCache, SemanticCache, NodeOutputCache — covered in the Cache and Memory post). Redis handles the real-time operational primitives that require single-digit millisecond latency and atomic operations:

  1. WebSocket token single-use enforcement — preventing replay attacks on session tokens
  2. Per-user chat rate limiting — protecting the platform from accidental and intentional overload
  3. WebSocket session registry — the foundation for future multi-worker horizontal scaling

Production Configuration

# docker-compose.yml
redis:
  image: redis:7-alpine
  container_name: getraind-redis
  hostname: redis
  restart: unless-stopped
  command: >
    redis-server
      --maxmemory 512mb
      --maxmemory-policy allkeys-lru
      --save ""
      --appendonly no
  networks:
    - backend
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 3s
    retries: 5

Every setting is deliberate:

Setting Value Why
maxmemory 512mb Hard cap Prevents Redis from starving other containers. All data stored here is ephemeral — if Redis evicts something, the worst case is a slightly degraded rate-limit window or a replay check that degrades gracefully.
maxmemory-policy allkeys-lru Evict least-recently-used When memory fills, evict the oldest data. No data stored here is irreplaceable — tokens expire naturally, rate counters reset every 60 seconds, session registry is rebuilt on reconnect.
--save "" Persistence off All data is ephemeral by design. Rate-limit counters and session tokens do not need to survive a Redis restart. Persistence would waste I/O with zero benefit.
--appendonly no AOF off Same reason — no durability requirement for ephemeral operational data.
No ports: mapping Not exposed to host Redis is only reachable from the backend Docker network. Exposing it to the host would be a security vulnerability.

Use Case 1: WebSocket Token Single-Use Enforcement

WebSocket tokens are HMAC-signed strings issued by the Portal with a 5-minute lifetime. Without Redis, a captured token could be replayed — an attacker who intercepts the token URL could open a second WebSocket connection impersonating the legitimate user.

The fix is atomic SET NX EX — set-if-not-exists with a TTL:

# Key: gtaif:ws_token:used:{hmac_signature}
# TTL: remaining seconds until the token's expiry timestamp

key    = f"gtaif:ws_token:used:{sig}"
result = await redis_client.set(key, "1", nx=True, ex=remaining_ttl)

if result is True:
    return username  # first use — OK
else:
    raise ValueError("Token already consumed — possible replay attack")

SET NX (set if not exists) is atomic in Redis — there's no race condition where two simultaneous connections could both read "not consumed" and both succeed. The first write wins; the second sees the key already exists and is rejected.

The TTL equals the token's remaining validity window. When the token would have expired anyway, the Redis key self-cleans. No separate cleanup job needed.

Graceful degradation: If Redis is unavailable, consume_ws_token() skips the replay check and validates only the HMAC signature and expiry timestamp. Token-based authentication still works; replay prevention is temporarily disabled. The platform logs gtaif.redis_unavailable and continues.


Use Case 2: Per-User Chat Rate Limiting

# Key: gtaif:rate:chat:{username}
# Default: 30 messages per 60-second sliding window

async def check_rate_limit(username: str, limit: int = 30, window: int = 60):
    key = f"gtaif:rate:chat:{username}"

    pipe = redis_client.pipeline()
    pipe.incr(key)
    pipe.ttl(key)
    count, ttl = await pipe.execute()

    if ttl < 0:
        # First message in this window — set the expiry
        await redis_client.expire(key, window)

    return count <= limit, count

Why this design?

The INCR + TTL pipeline is atomic — both commands execute in a single round trip. The TTL is only set on the first message of a window (when ttl < 0 means "key has no expiry"). Subsequent messages in the same window reuse the existing counter and TTL.

This is a sliding window from the user's first message, not a fixed clock boundary. A user who sends message 30 at 14:59:50 gets a fresh window at 15:00:50, not at exactly 15:00:00. This is intentional — it prevents "burst at the minute boundary" gaming.

Why rate limiting matters for enterprise AI: Without it, a single misconfigured process, an automated script, or a curious power user can exhaust your LLM API quota or pin your GPU inference capacity — affecting every other user. Rate limiting is the control mechanism that makes deploying to thousands of users safe.

Graceful degradation: If Redis is unavailable, all messages are allowed through. Rate limiting is disabled, not a hard failure.


Use Case 3: WebSocket Session Registry

# On WebSocket connection:
await redis_client.set(
    f"gtaif:ws:session:{session_id}",
    username,
    ex=86400   # 24-hour TTL — self-cleans stale entries
)

# On WebSocket disconnect (always runs in finally block):
await redis_client.delete(f"gtaif:ws:session:{session_id}")

# Reverse lookup (for cross-worker routing):
username = await redis_client.get(f"gtaif:ws:session:{session_id}")

Getraind currently runs as a single worker, so this registry isn't yet used for routing. But it's the essential foundation for horizontal scaling.

When multiple GTAIF workers run behind nginx, a chat message arriving at worker A might belong to a WebSocket connection held by worker B. The session registry tells a pub/sub router which worker owns which session — enabling cross-worker message routing without sticky sessions.

The infrastructure is in place. Activating multi-worker deployment requires adding the pub/sub routing layer on top of this foundation.


Key Namespace Summary

Key Pattern Value TTL Purpose
gtaif:ws_token:used:{sig} "1" Remaining token lifetime Consumed WS auth token — replay prevention
gtaif:rate:chat:{username} Request count (int) 60-second window Per-user rate counter
gtaif:ws:session:{session_id} username (str) 86400s (24h) Live session → owner mapping

Three key patterns. Three purposes. No overlap, no interference.


The Connexions Service: Separate Namespace

Getraind's Connexions service (connector metadata management) also uses Redis, but with a completely separate key namespace — id:{connection_id} — and the synchronous redis driver with RedisJSON.

# Connexions stores connector documents as JSON blobs
client.json().set(f'id:{connection_id}', Path.root_path(), document)
client.json().get(f'id:{connection_id}')
client.scan_iter('id:*')   # list all connections

Both GTAIF and Connexions share the same Redis container but never interfere — gtaif:* and id:* are completely disjoint namespaces.

One note for future work: Connexions uses the synchronous Redis driver, which blocks the event loop if called from an async context. As Connexions grows in scale, it should migrate to redis.asyncio to avoid this.


Graceful Degradation: The Full Picture

Every Redis use in GTAIF is designed to degrade gracefully — Redis unavailability is not a fatal failure, just a reduced security posture:

Redis Unavailable What Happens
WebSocket token replay check Skipped — tokens valid by HMAC+expiry only. Replay prevention disabled.
Rate limiting Disabled — all messages allowed through.
Session registry Registration skipped — no impact until multi-worker deployment.

The platform logs gtaif.redis_unavailable on startup if Redis can't be reached, and retries the connection periodically. Most functionality works without Redis; only security hardening and future scaling features require it.


Why This Matters for Enterprise Deployments

The Redis configuration described here is appropriate for Getraind's scale — a few hundred users, one Docker host, modest traffic. Enterprise deployments on Cisco UCS clusters with thousands of users need to think about this differently:

The architecture is the same. The operational care increases proportionally with scale.


What This Series Has Covered

This post completes the software stack series. Together, these eight posts cover the complete enterprise AI implementation — from business problem identification through every layer of the platform:

  1. Introduction — Why cloud AI is a trap and what to build instead
  2. LLM Gateway — Role-based routing, provider failover, 60–80% cost reduction
  3. GPU Hardware and Model Selection — The sizing math and tiered model strategy
  4. Enterprise AI Design — From business problem to agent topology
  5. Agent Factory — Declarative primitives and hot-reload compilation
  6. Agent Builder Runtime — The AI that designs AI
  7. Agent Chat Runtime — WebSocket to streamed response, end to end
  8. Cache and Memory — How the platform gets faster and smarter over time
  9. Redis (this post) — The operational glue

For the infrastructure that runs this stack — Cisco UCS, NVIDIA H100s, OpenShift, RoCE v2 networking — the enterprise infrastructure post covers every layer.

Questions, war stories, or implementations of your own? Reach out or find me on LinkedIn.

Share this post

More posts
Mahesh Pabba

Mahesh Pabba

AI & Cloud Architect · Cisco · 22+ years in enterprise infrastructure