The storage calculation is based on the complete data model, including core presence data and the stream-based session lifecycle mechanism. Sizing is presented per cluster to reflect the separation between the Database cluster and the Messaging cluster.
Storage Calculation (per user):
status:{user_id}): ~74 Bytessubscribers:{user_id}): ~768 Bytesuser_subscriptions:{user_id}): ~775 Bytesz:deadlines:{shard}): ~64 Bytes per userh:last_seen:{shard}): ~54 Bytes per userTotal per user (excluding stream): 1617 + 118 = ~1735 Bytes
Stream Storage (pings:{shard}):
The pings stream is a rolling log. Its size depends on the retention window, not the user count directly.
MAXLEN or MINID on the stream.Assumptions (capacity):
v (protocol version), event, payload, optional ts (server timestamp), optional id (message id), optional correlationId to pair responses to client commands.v only on breaking changes; add new fields in a backward‑compatible manner.id on requests; the server echoes this value as correlationId on any direct response (e.g., error) or hydration message (e.g., presence:updated sent in response to subscriptions:set). Server‑initiated events omit correlationId.The client sends commands to the server.
presence:updateUpdates the current user's presence status.
{
"v": 1,
"id": "req-1001",
"event": "presence:update",
"payload": {
"status": "online"
}
}
subscriptions:setReplaces the entire list of users the client is subscribed to.
{
"v": 1,
"id": "req-1002",
"event": "subscriptions:set",
"payload": {
"userIds": ["123", "124", "125"]
}
}
subscriptions:addAdds one or more users to the subscription list.
{
"v": 1,
"id": "req-1003",
"event": "subscriptions:add",
"payload": {
"userIds": ["126", "127"]
}
}
subscriptions:removeRemoves one or more users from the subscription list.
{
"v": 1,
"id": "req-1004",
"event": "subscriptions:remove",
"payload": {
"userIds": ["124"]
}
}
session:pingA heartbeat message sent periodically by the client to indicate it is still connected and active. This prevents the session from being marked as abandoned and cleaned up.
{
"v": 1,
"id": "req-1005",
"event": "session:ping"
}
session:disconnectA message sent by the client just before it intentionally disconnects (e.g., on logout or window close). This allows the server to perform an immediate and clean removal of the session without waiting for the grace period.
{
"v": 1,
"id": "req-1006",
"event": "session:disconnect"
}
notifications:subscribeSubscribes to a one-time notification for a specific event, like a user coming online. For scalability and performance, it will be implemented in the client application.
session:ping every 60s.429 with Retry-After.The server sends events to the client.
presence:updatedNotifies the client that one or more subscribed user's status has changed. Can also be sent in response to a subscriptions:set command to provide the initial state of the new subscription list.
{
"v": 1,
"event": "presence:updated",
"payload": {
"updates": [
{ "userId": "123", "status": "online" },
{ "userId": "124", "status": "idle" }
]
},
"ts": 1723972810,
"correlationId": "req-1002"
}
notifications:receivedDelivers a one-time notification that was previously subscribed to. For scalability and performance, it will be implemented in the client application.
errorReturns validation or processing errors for client commands.
{
"v": 1,
"event": "error",
"payload": {
"code": "INVALID_PAYLOAD",
"message": "userIds must be a non-empty array"
},
"ts": 1723972811,
"correlationId": "req-1002"
}
error event with correlationId set to the request id. On success, when a state change is relevant, the server emits presence:updated (e.g., after subscriptions:set) and includes the same correlationId.The data model is designed to be resilient and efficient. It separates the core presence data from the session lifecycle management mechanism, which is built on a reliable event stream pattern.

These structures hold the real-time state of user presence and subscriptions.
status:{user_id}subscribers:{user_id}user_subscriptions:{user_id}This model ensures reliable detection and cleanup of inactive sessions.
pings:{shard}): A durable stream where every client ping is recorded using XADD. This serves as the reliable input for session activity.z:deadlines:{shard}. A sorted set where the member is the user_id and the score is the Unix timestamp when the user should be considered stale if no new ping has arrived. This is updated by the consumer group.h:last_seen:{shard}. Stores the timestamp of the last processed ping for a user. Used by the Scheduler to prevent race conditions.presence_events:{shard}): An output stream where the Scheduler publishes inactive events, allowing other systems to react.Note: Sharding using a hash tag ({shard}) ensures related data for a user is co-located on the same cluster node, enabling efficient operations.
This section includes a description of the components that implement the system for fulfilling the requirements.
It's complemented with the C4 Model system context and container diagrams.

A Kubernetes cluster is used to run most of the components.
Includes components that handle system incoming requests.
A load balancer ensures load distribution and supports high availability sending requests to active nodes.
A TCP load balancer (L4) is used because no complex routing logic is needed. Once the WebSocket connection has been established, it is maintained as a persistent connection. If the connection fails, due to a communication problem or one of the component fails (API Gateway pod, online presence microservice), a new connection will be established by the client application.
An API Gateway is used to:
APISIX is the selected one because it's low latency, high throughput, high availability, stateless and extensible architecture. It is an Apache open source top project with good community and support. It will be implemented as a Kubernetes Ingress Controller.
Other good alternatives like Kong and NGINX were considered and APISIX selected because its features:
Kong has a stronger plugin ecosystem. APISIX, as a top‑level Apache Software Foundation project, is growing rapidly.
Contains the core applications and background workers that provide the business logic. All backend components will be implemented with Node.js, chosen for its scalable, lightweight, and efficient performance in I/O-bound, real-time applications.
An alternative like SpringBoot was considered but discarded due to its higher resource footprint and slower scaling for this specific use case.

| ComponentResponsibility | |
| Load Balancer (L4) | Routes WSS and IAM traffic to healthy nodes; performs TCP health checks and draining. |
| API Gateway (APISIX) | Terminates WSS, rate limits, validates JWT (JWKS/introspection), forwards to service. |
| IAM (Keycloak) | Authenticates users, issues tokens, exposes JWKS and introspection APIs. |
| Presence Microservice | Manages sessions and subscriptions; publishes updates; pushes notifications over WSS. |
| Database (Dragonfly) | Stores user status and subscription mappings. |
| Messaging Cluster (Dragonfly Pub/Sub) | Low-latency fan-out channel for presence updates. |
Provides authentication in a specific, specialized secure solution. Keycloak will be used to authenticate users and issue JWTs for establishing the WebSocket connection.
Keycloak is selected because it's an efficient, popular solution that helps to avoid lock-in. Proprietary features will be avoided, sticking to standards.
Other alternatives considered were Auth0/Okta and AWS Cognito. These SaaS solutions provide lower TCO but higher lock‑in and were discarded.
This service's primary role is to manage the persistent WebSocket connection with clients. Its responsibilities are:
subscriptions:*) to modify user data.The service will be implemented with Node.js because it provides a very scalable, lightweight solution ideal for I/O-bound real-time applications.
Spring Boot has been discarded because it is heavier for a real‑time application and slower to scale when needed.

| ComponentResponsibility | |
| Load Balancer (L4) | Routes WSS traffic from clients to the API Gateway. |
| API Gateway (APISIX) | Forwards authenticated WSS traffic; enforces coarse limits. |
| Presence Microservice | Receives pings and writes them to pings:{shard} stream. |
| Ping Consumers | Consume pings:{shard}, update last seen, and reschedule deadlines. |
| Scheduler | Finds expired deadlines; emits inactive events to presence_events:{shard}. |
| Cleanup Worker | Consumes inactive events and deletes the user's presence/session data. |
| Database (Dragonfly) | Holds h:last_seen:{shard} and z:deadlines:{shard} indexes. |
| Messaging Cluster (Streams) | Hosts durable pings and presence_events streams. |
A set of background workers that reliably process the activity event stream. They are responsible for updating the last seen status for each user and rescheduling their inactivity deadline. These will be implemented as a standard Kubernetes Deployment.
A background worker that periodically checks for users whose inactivity deadlines have passed. After verification, it publishes an "inactive" event to trigger the final cleanup. This will be implemented as a Kubernetes CronJob to ensure periodic execution.
A background worker that listens for "inactive" events. Upon receiving an event, it performs the full, permanent cleanup of all data associated with the stale user. This will be implemented as a standard Kubernetes Deployment.
A dedicated Dragonfly cluster that stores all core presence and session lifecycle data. Its in-memory, multi-threaded architecture was chosen for its low latency and high throughput, which are critical for this real-time application.
A separate, dedicated Dragonfly cluster used for the real-time, low-latency fan-out of presence updates. This isolates the critical messaging fabric from the database, ensuring high availability.
Using Dragonfly for both roles simplifies the technology stack and reduces operational overhead, which was preferred over introducing a different system like RabbitMQ or Kafka.
For reliable session management, we will use Dragonfly Streams. This feature provides a persistent, append-only log for processing activity events (pings) with at-least-once delivery guarantees.
The streams will be hosted on the same Dragonfly cluster as the Pub/Sub broker, creating a unified "Messaging Cluster" that is isolated from the primary database.
This layer collects and provides useful information for business analysts and SREs to monitor system health and usage.

| ComponentResponsibility | |
| Grafana | Unified dashboards for metrics, logs, and traces; SLO visualization. |
| Prometheus | Scrapes, stores, and queries metrics; feeds Alertmanager. |
| Alertmanager | Deduplicates, groups, and routes alerts to on-call. |
| Loki | Ingests and indexes logs; queried by Grafana. |
| Tempo | Stores distributed traces; queried by Grafana. |
| OpenTelemetry Collector | Receives OTLP telemetry; forwards traces and metrics (e.g., from CronJobs). |
| Fluent Bit | Collects pod logs and forwards to Loki. |
For our observability needs, we have selected the Grafana "LGTM" (Loki, Grafana, Tempo, Mimir/Prometheus) stack. As the company has a mature platform engineering team, this stack provides a cloud-native, highly available, and scalable solution that avoids vendor lock-in. It offers an efficient TCO at scale and provides a single pane of glass for metrics, logs, and traces, which is crucial for rapid troubleshooting.
OpenSearch stack has also been considered
A key metric to monitor will be the consumer lag for the pings stream (XPENDING).
This layer is responsible for the collection, storage, and querying of application logs and traces to support troubleshooting and debugging by SRE and DevOps teams.
We will leverage components from our chosen Shared Observability Stack:
XPENDING lag, consumer throughput, error rates.XPENDING, consumer restart storms, and publish failures.This section summarizes how HA is achieved for each component. The baseline target is ≥99.9% service availability.
Operational HA practices:
Communication with the client application is encrypted using WSS protocol and HTTPS (authentication flow).
In order to open a WebSocket connection and be able to access the system functionality, authentication is required with Keycloak. For increased security JWT is validated against the Keycloak token introspection endpoint.
API Manager provides rate-limiting and restricts message max size.
To protect Dragonfly unauthorized access the following measures are defined:
For preventing DDoS and other malicious attacks a Web Application Firewall can be used as first line of defense.
User authenticates with IAM, client obtains JWT, presents it in WSS handshake; API Gateway validates (introspection, fallback: local signature).
See diagrams/sd-user-login-and-token-validation.puml.

Establish WSS, validate JWT, load subscriptions and initial statuses, send presence:updated.
See diagrams/sd-user-connect-and-subscribe.puml.

Write status, publish on presence_updates:{user}, push presence:updated to subscribers.
See diagrams/sd-presence-update-fanout.puml.

Produce pings, process deadlines, emit inactive, delete state.
See diagrams/sd-session-cleanup.puml.

In order to provide high availability, the Kubernetes cluster will have nodes in 2 availability zones, and components distributed to have pods in both nodes.
The microservice is the primary interface for the client, handling the WebSocket connection and translating client commands into database operations and events.
SMEMBERS user_subscriptions:{user_id}.HGETALL on the status:* hashes).presence:updated message to the client to fill their initial view of their contacts' statuses.session:ping Handlingping, the service's only responsibility is to produce a message to the activity stream.XADD pings:{shard} MAXLEN ~ <per-shard-max> * user_id {user_id} ts {epoch_ms}MINID to enforce a strict time window (e.g., now−15m).subscriptions:set HandlingThis is the most complex client-facing operation, requiring updates to both the forward and reverse subscription mappings.
SMEMBERS user_subscriptions:{user_id}.added_users and removed_users by comparing the old list with the new one from the event payload.MULTI/EXEC):DEL user_subscriptions:{user_id} followed by SADD user_subscriptions:{user_id} [new_list].added_user_id, SADD subscribers:{added_user_id} {current_user_id}.removed_user_id, SREM subscribers:{removed_user_id} {current_user_id}.presence:updated message back to the client.session:disconnect Handling (Graceful Logout)disconnect message, the server can bypass the normal inactivity window and trigger an immediate cleanup.inactive event directly to the presence_events:{shard} stream, which will be picked up by the Cleanup Worker. This is much faster than waiting for the Scheduler to detect the inactivity.presence:updated per tick (e.g., every 50–100 ms) to reduce fan-out chatter.ts in broker payloads for freshness.The session lifecycle is managed by a set of decoupled components that implement a reliable, stream-based architecture.
These are a group of workers that process pings from the pings:{shard} stream with at-least-once delivery guarantees.
XREADGROUP.h:last_seen:{shard} hash and reschedules the user's inactivity deadline in the z:deadlines:{shard} sorted set.XACK. If a consumer fails, another can claim the pending messages, ensuring no pings are lost.This is a background worker that periodically checks for users whose deadlines have passed.
z:deadlines:{shard} set for users whose deadline score is less than the current time (ZRANGEBYSCORE).h:last_seen:{shard} hash to prevent race conditions.inactive event to the presence_events:{shard} stream and removes the user from the deadlines set.This worker listens to the presence_events:{shard} stream.
inactive event.subscribers sets, deletes their status hash and user_subscriptions set).The Dragonfly topology, for high availability, is 1 node (able to manage up to 1 TB of RAM) with 2 replicas, one in each availability zone.
If higher availability is needed in the future, a third replica could be deployed in a different region.
This configuration can scale much more than current needs.
Append Only File (AOF) persistence will be enabled for durability and being able to recover data in case of a full failure, and for faster fail-over recovery. It has small performance and throughput impact on Dragonfly thanks to its multithreaded architecture.
The same Dragonfly topology of the database is used for the same reasons.
presence_updates:{user_id}status:{user_id}).{user_id}.{ "status": "online" | "idle" | "offline" | "Busy" , "ts": 1712345678901 }ts for client-side freshness checks and dedupe.subscriptions:set|add|remove and on connect/disconnect.presence_updates:{shard}:{user_id}.The pings and presence_events streams are critical for reliable session management and must be durable. They will be hosted on the dedicated Messaging Cluster. To ensure data is not lost in case of a full cluster failure, Append Only File (AOF) persistence will be enabled for this cluster. This provides a strong guarantee of durability with minimal performance impact.
pings:{shard}session:ping.cg:pings (Ping Consumers).{ user_id: string, ts?: number } (HLD uses user_id; ts is optional and helps with observability/dedupe).XADD pings:{shard} MAXLEN ~ 50000000 * user_id {user_id} (optionally also ts {epoch_ms}).z:deadlines:{shard} and h:last_seen:{shard} updates.presence_events:{shard}session:disconnect (graceful).cg:presence-events.{ event: "inactive", user_id: string, reason?: "timeout" | "disconnect" }.XREADGROUP/XACK); consumers must be idempotent (e.g., re-check keys before delete).{shard} via a stable hash of user_id to co-locate related keys with the same hash tag.MAXLEN/MINID; keep a modest buffer (e.g., 10–15 minutes) sufficient for operational catch-up.XPENDING lag for cg:pings and cg:presence-events, XINFO STREAM, and consumer restart recovery time.For implementing fan-out notifications two options where considered:
Pub/Sub is selected following efficiency and performance requirements as it provides enough throughput for the expected load. For a more reliable and scalable solution, streams can be used.
Dragonfly Streams has been selected instead of Kafka/Kafka Streams or Flink for managing session expiration. This choice meets efficiency goals and reduces platform complexity/TCO by reusing the existing data tier while still providing low-latency, at‑least‑once processing via consumer groups and bounded retention.
JWT validation is performed when the WebSocket connection is established. The API Gateway calls the Keycloak token introspection endpoint to catch revoked/closed sessions without incurring per‑message overhead. If IAM is unavailable, the system falls back to local signature validation using cached JWKS with background refresh and a short grace window.
Mimir, a standard component of the Grafana LGTM stack, is not used because:
Regional blips/deploys cause thousands of clients to reconnect simultaneously, spiking gateway, IAM, and Dragonfly.
Mitigations:
Primary fail-over or hot keys increase latency, timeouts cascade to WS handlers.
Mitigations:
Bad rollout or overload blocks WS upgrades and HTTP.
Mitigations:
L4 or gateway idle timeout lower than ping interval silently drops WS.
Mitigations:
Very large follower sets overwhelm fan-out paths.
Mitigations:
Stalled consumers cause growing lag and stale presence.
Mitigations:
Token checks fail, blocking connects/API calls.
Mitigations:
Repeated failures block partitions and inflate PEL.
Mitigations: