# How RomM Manages Sessions with Redis: Custom ASGI Middleware Deep Dive

> Discover how RomM manages sessions with Redis using custom ASGI middleware. Learn about its efficient session storage and instant bulk revocation for stateless workers.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: deep-dive
- Published: 2026-07-05

---

**RomM stores authenticated user sessions entirely in Redis using a custom `RedisSessionMiddleware`, attaching session data to the ASGI scope and maintaining a user-specific session set to enable instant bulk revocation across stateless workers.**

The RomM application (rommapp/romm) implements a stateless session management layer that leverages Redis as the sole session store. By using a custom ASGI middleware, RomM eliminates the need for server-side in-process session storage, allowing multiple web workers to share session state seamlessly. This article explores how RomM manages sessions with Redis, covering the middleware lifecycle, data structures, and security mechanisms defined in the core source files.

## The RedisSessionMiddleware Architecture

The core of RomM's session management resides in [`backend/handler/auth/middleware/redis_session_middleware.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/middleware/redis_session_middleware.py). This custom ASGI middleware intercepts every incoming request to hydrate session data from Redis and processes outgoing responses to persist changes back to the cache.

### Session Lookup and Injection

When a request arrives, the middleware checks for a session cookie (default name defined in [`backend/handler/auth/constants.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/constants.py)). If present, it extracts the `session_id` and performs a Redis `GET` operation on the key `session:<session_id>`. The JSON-encoded session data is then parsed and injected into the ASGI `scope` dictionary as `scope["session"]`, making it accessible to downstream FastAPI handlers via the request object.

### Response Processing and Persistence

During response generation, the middleware handles three scenarios:

- **Session creation**: If new session data exists, the middleware generates a UUID, stores JSON-encoded data in Redis with key `session:<session_id>`, and sets a `Set-Cookie` header containing the generated ID.
- **Session update**: Modified existing sessions are written back to Redis with updated TTL using `ex=SESSION_MAX_AGE_SECONDS`.
- **Session deletion**: Empty sessions trigger deletion of the Redis key and immediate cookie clearance.

All Redis operations flow through the shared `async_cache` instance defined in [`backend/handler/redis_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/redis_handler.py), which wraps `redis.asyncio.AsyncRedis`.

## User-Session Sets for Bulk Revocation

Beyond individual session keys, RomM maintains a secondary data structure to track user sessions. For every authenticated user, the middleware creates a Redis set at `user_sessions:<user_id>` containing all active `session_id` values for that user.

This design enables **user-wide invalidation** through the `clear_user_sessions(user_id)` class method implemented in `RedisSessionMiddleware`. When invoked, this method retrieves all members from the user set using `SMEMBERS`, deletes each corresponding `session:<session_id>` key, and removes the set itself. This proves critical for security operations like password changes or administrative logout commands that require terminating all active sessions simultaneously.

## Configuration and Security Attributes

Session behavior is governed by `SESSION_MAX_AGE_SECONDS` in [`config.py`](https://github.com/rommapp/romm/blob/main/config.py), which drives both the Redis TTL and cookie `Max-Age`. The middleware applies secure cookie flags including `HttpOnly`, `SameSite=lax`, and optional `Secure` attributes, ensuring session tokens never leak via XSS or insecure transport.

Because session data lives exclusively in Redis, the application achieves true statelessness—any worker process can handle any request without sticky session requirements, enabling horizontal scaling across multiple RomM instances.

## Practical Implementation Examples

### Accessing Session Data in FastAPI Endpoints

Downstream handlers retrieve session data via the ASGI scope injected by the middleware:

```python
from fastapi import Request

async def whoami(request: Request):
    # The middleware populates request.scope["session"]

    session = request.scope.get("session", {})
    user_id = session.get("sub")  # 'sub' contains the user identifier

    return {"user_id": user_id, "session_data": session}

```

### Clearing All Sessions for a User

To revoke every session belonging to a specific user (e.g., after password change):

```python
from handler.auth.middleware.redis_session_middleware import RedisSessionMiddleware

async def logout_user_everywhere(user_id: str):
    # Removes all session keys and the user set from Redis

    await RedisSessionMiddleware.clear_user_sessions(user_id)

```

### Manual Session Creation

While the middleware handles session lifecycle automatically, manual creation follows this pattern:

```python
import uuid
import json
from handler.redis_handler import async_cache
from config import SESSION_MAX_AGE_SECONDS

async def create_manual_session(user_id: str, extra_data: dict):
    session_id = str(uuid.uuid4())
    data = {"sub": user_id, **extra_data}
    
    # Store session with TTL

    await async_cache.set(
        f"session:{session_id}",
        json.dumps(data),
        ex=SESSION_MAX_AGE_SECONDS,
    )
    
    # Track in user set

    await async_cache.sadd(f"user_sessions:{user_id}", session_id)
    return session_id

```

## Summary

- RomM uses `RedisSessionMiddleware` in [`backend/handler/auth/middleware/redis_session_middleware.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/middleware/redis_session_middleware.py) to manage stateless sessions entirely in Redis.
- Session data is stored under `session:<session_id>` keys with JSON encoding and TTL expiration based on `SESSION_MAX_AGE_SECONDS`.
- User-session sets (`user_sessions:<user_id>`) enable bulk revocation via the `clear_user_sessions` method.
- All Redis interactions use the shared `async_cache` from [`backend/handler/redis_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/redis_handler.py).
- Secure cookie attributes (`HttpOnly`, `SameSite=lax`) protect the session token in browser storage.

## Frequently Asked Questions

### How does RomM handle session expiration?

RomM leverages Redis TTL (time-to-live) set via the `ex` parameter during `SET` operations. The TTL value equals `SESSION_MAX_AGE_SECONDS` from [`config.py`](https://github.com/rommapp/romm/blob/main/config.py), ensuring stale sessions are automatically purged from memory without explicit cleanup logic or background jobs.

### Can multiple RomM workers access the same user session?

Yes. Because sessions reside entirely in Redis rather than local memory, any worker process can retrieve session data using the `session_id` from the cookie. This eliminates the need for sticky sessions or shared memory between workers, supporting horizontal scaling.

### What happens when a user logs out?

The middleware detects empty session data and removes the Redis key `session:<session_id>`. Additionally, calling `RedisSessionMiddleware.clear_user_sessions(user_id)` removes all active sessions for that user by iterating through their `user_sessions:<user_id>` set and deleting each corresponding session key.

### Where is the session cookie name configured?

The cookie name and related constants are defined in [`backend/handler/auth/constants.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/constants.py), with the default value typically set to `session`. The middleware references these constants when reading request cookies and setting `Set-Cookie` headers in responses.