you are expert in C++, networking, backend/systems...

Criado em: 4 de agosto de 2026

Respondido usando GPT-5.6 Thinking por Chat01

Pergunta

you are expert in C++, networking, backend/systems architect, AAA-scale multiplayer game backends, redis, connection between microservices

1-before help me to design backend game server, i want deep search about game GUNROX to know its UI design and how it works inside, and focus on UI controls like add friend, join clan, create game join game move maps..etc

after know everything about gunrox we can start

2- more info about game UI controls:

1- create game and waiting for players to join to start game

2- start random queue(match making)

3- send friend request, accept friend, add players to ignore list

4- there are fixed maps like "lost beach" "ice valley" total in my game like 8-10 maps

5- there are global list all players online in current map for example when login and im in "lost beach" map and it has 1k players online, i should get a list of 1k players usernames and their user_ids ( because usernames just for human display looking but user_ids is to sending messages like ["hi"][5 ( its user_id for player besho13)]

6- i use user_id is fixed and primary for each player creating account to use it to save player data inv and game history..etc, also used to send messages like ["hi"][target_id:5], so im thinking to use that if yes tell me if bad tell me too i need truth for AAA game

7- there are simple shop just go and buy items this happends much like every 10-15 minutes user buy items for start game.

8- there are trading system between 2 players peer-to-peer for trading items

9- there are global chat for each map private chat, trade-chat should send to all online players, game chats for each game..etc

9- there are notifications but this rarley like once a day from system for like tournament

3- notes:

1-please dont search for how GUNROX backend works i dont want like that i want best for latest tech use and best for scale AAA games just search GUNROX to know its features like add friend, clan system..etc

2- client is always C++ native desktop work for windows for now so im able to use anything not confined to web protocols

4- here is my microservices i designed

  1. auth_server

Protocol: HTTPS (REST/JSON)
Client-facing: Yes (short-lived — connection closes after login/register completes)

Responsibilities:

Register new account — email, username, password (Argon2id hash)

Login — verify email/password against Postgres

Receive device_fingerprint (SHA-256 hash, computed client-side) at login and store it bound to the session

Generate the session token — 256-bit CSPRNG (RAND_bytes), hex-encoded, opaque (no embedded user_id, no signature, no claims)

Write session to Redis: SET session:{token} {user_id, device_fp} EX 3600

Return {token, user_id} to client

Owns the users table (email, username, password_hash, status, failed_attempts, locked_until) — no other service touches this table

Rate-limit login attempts (per-IP and per-account, via Redis INCR/EXPIRE) — first line of defense against credential stuffing

Does NOT do: issue refresh tokens (deferred), talk to lobby_server/chat_server directly (no coupling — its job ends at the Redis write), hold any session state itself after responding.

  1. lobby_server

Protocol: raw TCP, C++ Asio, custom binary framing
Client-facing: Yes (persistent connection, held for the whole play session)

Responsibilities:

Validate session on connect: GETEX session:{token} EX 3600 against Redis, compare device_fingerprint, reject on mismatch

Maintain local unordered_map<socket_fd, user_id> (+ reverse map) — rebuilt fresh on every connect/reconnect, never persisted

Create game (open a waiting room, hold slots for players to join)

Forward "start queue" requests to matchmaking_server

Friend system: send request, accept, decline, remove, add to ignore list

Clan actions: create, join, leave, invite, kick, promote

Shop: browse, purchase (writes to Postgres inventory/transactions)

Trade: initiate trade session, negotiate offer

..etc

  1. chat_server

Protocol: raw TCP, C++ Asio, custom binary framing
Client-facing: Yes (persistent, separate connection from lobby)

Responsibilities:

Validate session the same way as lobby_server (own independent GETEX call)

Maintain local unordered_map<socket_fd, user_id> (own copy, independent of lobby_server's)

Maintain local RAM cache built from presence_events subscription: map_members[map_name] -> set<user_id>, and per-player current_map / in_game / game_id

Route messages purely by user_id, never username: global/map chat, trade-chat broadcast, private DM ([text][target_user_id]), per-game chat (route by matching game_id between sender and recipient)

Filter broadcast delivery: skip any recipient whose local cache says in_game or offline

Enforce ignore list at delivery time (silently drop, no error to sender — matches the real Gunrox pattern)

..etc

  1. presence_server

Protocol: raw TCP, C++ Asio, custom binary framing
Client-facing: Yes (own dedicated connection — separated specifically so bulk roster payloads never contend with lobby_server's latency-sensitive shop/trade traffic)

when client is login in map for example lost_beach and has 1k online players presence_server should send a list of all 1k players usernames:user_ids

also when move to another map for example ice_valley and has 2k player so should send to client 2k usernames:user_ids, also if some players dc so should send to all players in that map players dc..etc

  1. matchmaking_server

Protocol: internal-only TCP, C++ Asio, custom binary framing
Client-facing: No — only lobby_server talks to this

should matching players and tell lobby_server player2 player3 are matching and lobby_server starting game giving order to game_Server to start it or matchmaking_server send direct to game_server to start it still thinking about it

  1. game_server

Protocol: raw TCP, C++ Asio, custom binary framing (already built)
Client-facing: Yes (only once matched — client connects here with {game_id, ticket})

its everything inside game for each game 2-10 players has instance object inside game_server to handle that game

Task: Design Redis + Local RAM Architecture for a AAA Game Backend

Context

I'm designing the backend for a AAA game in the data management field. The backend has 6 microservices, and some of them run as multiple scaled instances (e.g. chat_server1, chat_server2, etc). I need help designing:

The connection/data pattern between the 6 microservices

When to use local RAM vs when to use Redis, and how to use each

What data types to use in each case

How this holds up when scaling to multiple instances of the same service (e.g. chat_server1, chat_server2, ...)

Known requirements

After a successful login, the client gets a session_id to connect to lobby_server, chat_server, and presence_server. This session_id is saved in Redis with a 1 hour expiry, and it renews.

For game_server, login uses a one-time-use ticket with a 30 second expiry.

Each player has a status: online, offline, away, in_game, in_trade, in_shop.

Each player also has a current map (e.g. lost_beach, ice_valley, etc).

Why player status + map matters (this is the core of what I need solved)

Servers need to know a player's status and map because:

Chat example: if player1 sends ["hi all"] in global chat while in map lost_beach, chat_server needs to know all players who are online in map lost_beach and NOT in_game, so it can fan-out this message to all of them. This needs to be fast, with no lag.

Lobby example: lobby_server needs to know if player4's status is in_shop, in_game, or in_trade, so that player2 cannot send player4 a trade invite while player4 is in one of those states.

Multi-instance example: if there are 2 chat_servers — chat_server1 has player1, player2, player3 connected via TCP, and chat_server2 has player4, player5, player6 connected via TCP — how do we handle: (a) player1 sending a direct message to player5, who is on a different chat_server instance, and (b) player1 sending a global message ["hi all"] when players 1–6 are all in the same map and none are in_game, so chat_server1 needs to deliver to players 1–3 and chat_server2 needs to independently deliver to players 4–6.

What I need

Please design the full Redis pattern and data structures for all of this — including where pub/sub or any other Redis features should be used — plus the local RAM design for each server. For local RAM: each server (lobby, chat, etc.) needs, at minimum, an unordered_map to bind a player's TCP socket to their user_id (socket <-> user_id). Also design how Redis stores all logged-in player sessions with the 1 hour expiry and renewal described above.

Please think through this deeply and critically, and cover all the scenarios above — the chat fan-out case, the lobby status-check case, and the multi-instance routing case — so the Redis design and local RAM design are complete for each server.

Pensamento

Searched 11 websites

Worked for 3m 46s

Resposta

Images:

Gabix asking me to fake - gunrox.com a silence please - GUNROX.com Evidence of bad language/breaking game rules - gunrox.com Gunrox - Download

1. What GUNROX’s public UI tells us

I could verify the important product flows, although public material does not document every exact button or the current implementation behind it. I am treating these as visible product requirements, not as a model for your backend.

Confirmed GUNROX-style flow

The main social flow appears to be:

text
Login World/social location ├─ Location player roster ├─ Location/lobby chat ├─ Private chat tabs ├─ Trade chat ├─ Player profile / squad / inventory ├─ Friends / ignore ├─ Clan ├─ Cash shop └─ Game lobby ├─ Browse open games ├─ Create game ├─ Join waiting room ├─ Random battle └─ Start match

Screenshots show a world-map/location screen with a right-side player roster, chat tabs below, top navigation buttons and Cash Shop access. The lobby shows rooms with fields such as game type, teams, creator, turn time and player count. Recent official news also confirms that the lobby is still being redesigned around selectable game modes and joinable game sessions. (gunrox.com)

Public GUNROX material confirms several chat contexts: lobby, private, clan, in-game and trade chat. An older player guide describes selecting a player from the roster to send a private message, private-message shortcuts, team chat inside a match and private messages continuing while someone is in a game or another activity. The official anti-toxicity update separately mentions private, clan, lobby, in-game and trade messages. (gunrox.com)

GUNROX has had random battle modes, including a documented 2v2 random battle mode. (gunrox.com)

Clan creation and invitations are also confirmed. Historically, clan creation appeared on the squad tab after purchasing the feature, followed by a creation form and clan icon upload. (gunrox.com)

Public location names include Beginner Quarter, Clover Quarter, Lost Beach, Junkyard, Mystic Forest, Memorial Boulevard and Ice Valley. The location is not merely a visual map: it affects available players, chat context and item progression. (gunrox.com)

Product model to preserve

For your game, I would preserve these concepts:

  • A social location such as lost_beach, containing online players, chat and open rooms.
  • A battle map/template, which is separate from the social location.
  • Player interactions from roster entries: profile, whisper, friend request, ignore, clan invite, trade invite.
  • Two game-entry paths:
    • manually created waiting room;
    • matchmaking queue.
  • Persistent social identity across maps and matches.
  • Server-authoritative inventory, trading and match outcomes.

Do not reproduce old GUNROX technical limitations. Reproduce the interaction model.


2. Direct verdict on your current design

Your service separation is a good starting point, but I would change several important parts before calling it AAA-ready.

What is correct

Stable user_id

Using a fixed immutable ID is correct.

Use:

cpp
using UserId = std::uint64_t;

In PostgreSQL it can be a positive BIGINT. Never route or authorize using usernames. Usernames are display properties and may change.

For better privacy, consider having two IDs:

text
account_id Internal PostgreSQL primary key public_player_id Opaque identifier exposed to clients

A sequential ID is not automatically insecure, but it makes account enumeration easier. Authorization must never depend on IDs being difficult to guess.

PostgreSQL for durable ownership

Inventory, currency, friends, ignores, clans, completed trades and game history belong in PostgreSQL.

Redis for ephemeral state

Sessions, online state, routing, temporary rooms, matchmaking tickets, rate limits and short-lived tickets are appropriate Redis data.

Local RAM for active connections

Socket bindings, local player collections, owned room actors and active game instances should live in RAM.

What needs changing

1. Do not model player status as one enum

These are not mutually exclusive concepts:

text
online / offline / away in_game / in_trade / in_shop current map

Use separate dimensions:

cpp
enum class Connectivity : std::uint8_t { Offline, Online, Away }; enum class Activity : std::uint8_t { Idle, WaitingRoom, Matchmaking, Shop, Trade, Game }; struct PlayerRuntimeState { UserId user_id; Connectivity connectivity; Activity activity; std::uint16_t social_map_id; std::uint64_t room_id; std::uint64_t trade_id; std::uint64_t game_id; std::uint64_t version; std::uint64_t lease_expiry_ms; };

A player can therefore be:

text
connectivity = Away activity = Shop map = Lost Beach

This is substantially safer than adding more values to one giant status enum.

2. One service must own runtime-state transitions

Lobby, matchmaking, presence and game servers must not independently write player state.

Your presence_server should contain an internal Player State authority. Other services send commands such as:

text
TryEnterShop TryEnterTrade TryJoinRoom TryEnterQueue TryEnterGame LeaveGame MoveSocialMap

The state authority validates transitions and publishes resulting changes.

This prevents:

text
Lobby says player is in trade Matchmaking says player is queued Game server says player is in game

from happening simultaneously.

3. Your lobby service has too many permanent responsibilities

It currently owns:

  • rooms;
  • social relationships;
  • clans;
  • shop;
  • trades;
  • matchmaking frontend.

This is acceptable for an early implementation, but internally separate these into modules with clear ownership:

text
LobbyGateway RoomCoordinator SocialService EconomyService TradeCoordinator MatchmakingFrontend

They can initially run in the same process. Splitting code ownership now makes later service extraction much easier.

4. Three persistent client connections are possible, but not ideal

Your current design:

text
Client → lobby TCP Client → chat TCP Client → presence TCP Client → game TCP when matched

will work, but an AAA-style architecture normally benefits from one persistent edge connection:

text
Client → Edge Gateway ├─ lobby messages ├─ chat messages └─ presence messages Client → Game Server after assignment

A single gateway gives you:

  • one authentication exchange;
  • one heartbeat;
  • one reconnect path;
  • one outbound congestion-control point;
  • fewer NAT and firewall problems;
  • simpler session revocation;
  • no disagreement about which of three sockets means the player is online.

You can still keep lobby, chat and presence as independent internal services.

If you retain the three sockets, the rest of the design below still works.

5. Do not use raw socket_fd as the permanent connection identity

File descriptors are reused after sockets close.

Use:

cpp
using ConnectionId = std::uint64_t; std::unordered_map<ConnectionId, std::shared_ptr<ClientConnection>> connections_by_id; std::unordered_map<UserId, std::weak_ptr<ClientConnection>> connection_by_user;

The socket lives inside ClientConnection. Generate a monotonically increasing or random ConnectionId.


3. Recommended system topology

text
┌──────────────────┐ │ auth_server │ │ HTTPS + TLS │ └────────┬─────────┘ │ session ┌──────────────┐ ┌──────────────────────┐ │ C++ client │───────▶│ Edge/Gateway layer │ │ TCP + TLS │ │ persistent connection│ └──────┬───────┘ └──────────┬───────────┘ │ │ │ game ticket ├──────── lobby/room │ ├──────── chat ▼ ├──────── presence/state ┌────────────────┐ └──────── social/economy │ game_server │ │ game instances │◀──── game allocator/director └────────────────┘ ▲ matchmaking_server Shared infrastructure: PostgreSQL durable authoritative data Redis Cluster ephemeral state, leases, routes, queues, caches NATS low-latency service events and realtime routing

For internal service communication I recommend:

text
Request/reply commands: gRPC/Protobuf or NATS request/reply Realtime fan-out: NATS Core Reliable events/jobs: NATS JetStream or Redis Streams Ephemeral state/cache: Redis Durable business data: PostgreSQL

NATS subjects give location-independent publish/subscribe routing. Core NATS is ephemeral, while JetStream adds persisted streams, sequence numbers and acknowledged consumers. (NATS Documentation)

A Redis-only version is possible, but for serious scale I would not make Redis handle both all state and all internal messaging.


4. Redis versus RAM versus PostgreSQL

PostgreSQL: source of truth

Store these durably:

text
users player_profiles username_history inventory_items wallet_balances shop_transactions friend_requests friendships ignore_edges clans clan_members completed_trades trade_transaction_items game_results game_history notifications

Redis: recoverable ephemeral state

Store:

text
sessions service connection routes short-lived connection tickets one-time game tickets runtime player state room ownership leases room snapshots matchmaking tickets and indexes rate limits idempotency windows cached profile cards temporary trade state

Local RAM: live operational state

Store:

text
active sockets send queues local user-to-connection bindings local map membership local chat eligibility room actors owned by this instance matchmaking indexes owned by this instance active trade actor state active game simulations recent message deduplication IDs

Rule

Redis is not a substitute for local RAM on every message.

A map-chat message must not perform:

text
SMEMBERS map GET player status × 1,000 GET route × 1,000 PUBLISH × 1,000

That architecture will be slow and expensive.

Redis provides bootstrap, coordination and recovery. RAM handles the hot path.


5. Session architecture

Session token

Your 256-bit CSPRNG token is good. Hex encoding is valid, although Base64URL is shorter.

Do not store the raw token in the Redis key. Store its SHA-256 digest:

text
Client token: random 256-bit secret Redis key: sess:{SHA256(token)}

If Redis keys are accidentally logged or inspected, the visible value is not itself the bearer token.

Redis representation

Use a Redis string containing a compact Protobuf or MessagePack object:

text
Key: sess:{9be3...} Type: STRING Value: SessionRecord { user_id device_fingerprint auth_epoch issued_at_ms absolute_expiry_ms } TTL: 3600 seconds

A string is important because GETEX reads a string and renews its expiry in one operation. (Redis)

Pseudo-operation:

text
GETEX sess:{token_hash} EX 3600

Then:

  1. decode;
  2. verify user status;
  3. compare device fingerprint;
  4. check absolute_expiry_ms;
  5. accept connection.

Do not allow infinite sliding lifetime

A one-hour sliding expiry can live forever if the token is stolen and continuously used.

Use both:

text
idle timeout: 1 hour absolute lifetime: for example 24 hours

A future refresh-token design can replace this.

Device fingerprint warning

A SHA-256 device fingerprint computed by the client is not a cryptographic proof of device identity. A modified client can copy or forge it.

Use it as:

text
risk signal account-security telemetry concurrent-session indicator

Do not use it as the primary protection for a stolen bearer token. Bearer tokens should ideally be audience-restricted so that leakage from one service has limited value. (OWASP Cheat Sheet Series)

Better service connection flow

Instead of sending the master session token to lobby, chat and presence:

text
1. Client authenticates master session. 2. Gateway/auth exchange issues: - lobby connection ticket - chat connection ticket - presence connection ticket 3. Each is valid for 30–60 seconds and one use. 4. Service consumes it with GETDEL.
text
ct:{ticket_id} STRING ConnectionTicket EX 60

GETDEL atomically returns and deletes a string, making it suitable for one-time tickets. (Redis)

If you retain the shared session token for now, it is workable, but put all connections behind TLS and implement immediate revocation.

Renewal

Do not call GETEX on every packet.

For a persistent connection:

text
heartbeat: every 10–20 seconds route lease refresh: every 10–15 seconds session renewal: every 5–10 minutes

Only one component should renew the master session. With an edge gateway, that is the gateway.

Logout and revocation

On logout or account ban:

text
DEL sess:{token_hash} INCR auth_epoch:{user_id} PUBLISH session.revoke user_id

Each connected server closes matching connections. Because live Pub/Sub can lose messages during disconnection, servers must also compare auth_epoch periodically or when reconnecting.


6. One-time game-server ticket

Use one ticket per player:

text
gt:{ticket_id} STRING { user_id game_id server_id connection_nonce issued_at_ms } EX 30 NX

The client connects to the assigned game server and sends:

text
game_id ticket_id

The game server runs:

text
GETDEL gt:{ticket_id}

Then verifies:

text
ticket.game_id == requested_game_id ticket.server_id == this_server_id ticket.user_id is expected by game instance

A consumed ticket cannot be replayed.

For reconnects, mint a new reconnect ticket rather than restoring the previous ticket.


7. Connection-routing directory

Every client-facing service instance gets an immutable node ID:

text
chat-eu-17 lobby-eu-08 presence-eu-05

Redis keys

text
route:chat:{12345} STRING { node_id connection_id connection_epoch } EX 45 route:lobby:{12345} route:presence:{12345}

Keep the route and epoch in the same Redis hash slot:

text
conn_epoch:chat:{12345} route:chat:{12345}

On connection:

text
epoch = INCR conn_epoch:chat:{12345} SET route:chat:{12345} {node_id, connection_id, epoch} EX 45

Refresh the route while the connection is alive.

On disconnect, do not blindly delete the route. The player may already have reconnected to another node. Use a Redis Function:

text
Delete route only when stored epoch == disconnecting epoch

Redis Functions execute atomically. In Redis Cluster, related keys must share a slot; hash tags such as {12345} allow this. (Redis)


8. Authoritative player runtime state

Redis key

text
pstate:{12345} HASH connectivity online activity idle map_id 3 room_id 0 trade_id 0 game_id 0 version 184 owner_node state-eu-4 lease_expiry_ms 1785860400000 EX 60

Redis is a recovery snapshot here. The current owner keeps the state in RAM.

State-machine examples

Allowed transitions:

text
Idle → Shop → Idle Idle → Trade → Idle Idle → WaitingRoom → Game → Idle Idle → Matchmaking → Game → Idle Idle → another social map

Rejected transitions:

text
Game → Trade Trade → Matchmaking Shop → Game without leaving shop WaitingRoom → Trade

Every successful transition increments version.

All commands should support optimistic concurrency:

text
TryEnterTrade { user_id expected_state_version trade_id }

Response:

text
Success { new_version } Conflict { current_state, current_version }

The version prevents delayed commands from changing newer state.


9. Presence and map rosters

Map ownership

Do not have every presence-server instance independently mutate every map.

Partition map actors by:

text
(region, social_map_id, map_shard)

For example:

text
(eu, lost_beach, 0) → presence-map-owner-2 (eu, ice_valley, 0) → presence-map-owner-5

A single actor serializes joins, leaves and state changes for that map shard.

With only 8–10 maps and roughly 1,000–2,000 players per map, this is straightforward. If a map becomes much larger, create multiple invisible social shards:

text
lost_beach/0 lost_beach/1 lost_beach/2

The UI may continue to display “Lost Beach.”

Redis map snapshot

Keep related keys together:

text
map:{eu:lost_beach:0}:alive ZSET member = user_id score = lease_expiry_ms map:{eu:lost_beach:0}:roster HASH field = user_id value = packed PlayerCard map:{eu:lost_beach:0}:seq STRING integer

PlayerCard might contain:

cpp
struct PlayerCard { UserId user_id; std::string username; std::uint16_t level; std::uint64_t clan_id; std::uint32_t profile_version; };

A sorted set is useful because stale members can be removed by expiry score:

text
ZREMRANGEBYSCORE map:{...}:alive -inf now_ms

Do not refresh the roster to every client on every heartbeat.

Initial roster protocol

When entering Lost Beach:

text
Server → RosterSnapshotBegin { map_id, snapshot_seq, total_count } Server → RosterSnapshotChunk { snapshot_seq, entries[100..250] } Server → RosterSnapshotEnd { snapshot_seq }

After that:

text
RosterDelta { seq, joined[], left[], updated[] }

If the client receives sequence 105 after 103:

text
Client → RequestRosterResync

This protects you against lost messages and reconnect races.

A roster of 1,000 IDs and usernames is only tens of kilobytes with a binary protocol, so the initial snapshot is reasonable. Chunking prevents one large frame from blocking other traffic.

Batch high-churn deltas

Rather than sending one network packet for each join or leave, aggregate for approximately 50–200 milliseconds:

text
joined: [14 players] left: [9 players] updated:[3 players]

This substantially reduces syscalls during reconnect storms.


10. Chat architecture

Important optimization

Each chat-server instance does not need a complete list of every player in the world.

It needs:

text
all locally connected chat users their current map and activity local users grouped by map local ignore filtering data route cache for recent DM targets

The presence service owns the global roster. The chat node only fans out to its own connected sockets.

Chat-server RAM

cpp
struct LocalChatUser { UserId user_id; ConnectionId connection_id; std::uint64_t connection_epoch; std::uint16_t map_id; Activity activity; std::uint64_t game_id; std::shared_ptr<const IgnoreSet> ignored_users; }; std::unordered_map<ConnectionId, LocalChatUser> by_connection; std::unordered_map<UserId, ConnectionId> by_user; std::unordered_map<MapId, LocalUserSet> local_users_by_map; std::unordered_map<MapId, LocalUserSet> local_chat_eligible_by_map; std::unordered_map<UserId, LocalUserSet> local_recipients_ignoring_sender;

The last inverse index makes broadcast filtering efficient:

text
sender 123 is ignored by local users {9, 17, 28}

No Redis lookup is needed per recipient.

Map/global chat fan-out

Suppose:

text
chat_server1: users 1, 2, 3 chat_server2: users 4, 5, 6 all are in Lost Beach

Player 1 sends:

text
["hi all"]

Flow:

text
1. chat_server1 validates rate limit and sender state. 2. chat_server1 publishes one envelope: subject: chat.map.eu.lost_beach.0 { message_id, sender_id: 1, sender_name, map_id, text, created_at } 3. chat_server1 receives the subject and sends to local users 1–3. 4. chat_server2 receives the same subject and sends to local users 4–6. 5. Each node skips: - users no longer in that map; - users currently in_game, if that is your rule; - users who ignore sender 1; - slow/disconnected sockets.

This produces:

text
1 broker publish N chat nodes receiving N local iteration loops 6 final socket writes

It does not produce six Redis messages.

Dynamic subscriptions

A chat node should subscribe to a map subject only while it has local users in that map.

text
First local user enters Lost Beach: subscribe chat.map.eu.lost_beach.0 Last local user leaves Lost Beach: unsubscribe chat.map.eu.lost_beach.0

Direct messages across chat instances

Player 1 on chat_server1 sends to player 5 on chat_server2.

text
1. chat_server1 checks local route cache. 2. On miss, GET route:chat:{5}. 3. Result: node_id = chat_server2 epoch = 78 4. Publish to: chat.node.chat_server2 5. Envelope contains: target_id = 5 expected_connection_epoch = 78 6. chat_server2 verifies: - user 5 is local; - connection epoch is 78; - user 5 does not ignore user 1. 7. Deliver to user 5.

If user 5 reconnected between steps:

text
chat_server2 returns ROUTE_STALE chat_server1 invalidates cache chat_server1 resolves route again once

Do not retry indefinitely.

Redis-only broker version

You could use:

text
SPUBLISH chat:{map:eu:lost_beach:0} payload SSUBSCRIBE chat:{map:eu:lost_beach:0} SPUBLISH chat:{node:chat-eu-17} payload

Redis Pub/Sub is at-most-once: disconnected subscribers miss messages. Sharded Pub/Sub limits propagation to the relevant Redis shard and scales better than global cluster Pub/Sub. (Redis)

That is acceptable for transient map chat.

It is not acceptable for:

text
completed purchases trade commits friend acceptance match assignments permanent notifications

Use durable state plus Streams/JetStream for those.

Backpressure

Every connection needs a bounded outbound queue:

text
max queued bytes max queued chat messages oldest-message age

For a slow client:

text
1. Coalesce roster updates. 2. Drop nonessential typing/presence events. 3. Drop old chat messages if product permits. 4. Disconnect if the queue remains over limit.

Never let one slow socket block an Asio I/O thread.

Per-game chat

Since a game server already owns all 2–10 game connections, game chat should normally be handled by the game server.

Advantages:

  • correct ordering relative to game actions;
  • no external chat dependency during a match;
  • trivial fan-out to 2–10 sockets;
  • easier team-chat authorization.

The game server can asynchronously send chat records to moderation/logging storage.

Keeping game chat in chat_server is possible, but it creates an unnecessary dependency.


11. Lobby availability and trade-invite checks

Do not implement:

text
GET target status if idle: send invite

That has a race:

text
Lobby reads target = idle Target enters matchmaking Lobby sends trade invite

Use the local state cache only for an early UI or performance rejection.

The authoritative flow is:

text
1. Player 2 requests trade with player 4. 2. Lobby performs quick local-cache check. 3. Lobby sends CreateTradeInvite to TradeCoordinator. 4. TradeCoordinator asks Player State authority to validate target. 5. State authority atomically checks current state/version. 6. If allowed, invite is created. 7. A targeted event is routed to player 4.

On acceptance:

text
1. Reserve lower user_id first. 2. Reserve higher user_id second. 3. Both must transition Idle → Trade. 4. If the second reservation fails, release the first. 5. Create trade actor.

This deterministic ordering prevents circular reservation deadlocks.

Do not attempt to atomically update arbitrary player A and player B keys with one Redis Cluster script. Arbitrary user keys normally live in different hash slots. Redis transactions and scripts generally require related keys to be in the same slot. (Redis)

Cross-player coordination belongs in a service-level coordinator.


12. Trading system

Although the UI looks peer-to-peer, the transaction must be server-authoritative.

Trade actor

cpp
struct TradeSession { TradeId trade_id; UserId player_a; UserId player_b; Offer offer_a; Offer offer_b; std::uint64_t revision; bool confirmed_a; bool confirmed_b; TradeState state; };

Every offer modification:

text
increments revision clears both confirmations

Confirmation includes the exact revision:

text
ConfirmTrade { trade_id, revision }

A stale confirmation is rejected.

Temporary Redis record

text
trade:{trade_id} STRING packed TradeSessionSnapshot EX 900

This is for recovery, not final ownership.

Final PostgreSQL transaction

When both players confirm the same revision:

text
BEGIN lock involved inventory rows in deterministic item_id order verify every item still belongs to its offered owner verify every item is tradable verify currency balances verify trade_id has not already committed transfer item ownership transfer currency insert completed trade insert trade line items insert outbox events COMMIT

Use an idempotency constraint on trade_id.

PostgreSQL provides row locking and serializable isolation, but applications using serialization must be prepared to retry serialization failures. (PostgreSQL)

A Redis failure must never create or duplicate an inventory item.


13. Shop architecture

Your purchase frequency is low. The shop is not a Redis-performance problem.

Redis

Cache:

text
shop catalog item definitions price versions availability windows per-user purchase rate limits

PostgreSQL transaction

Every purchase request includes:

text
request_id catalog_version item_id quantity expected_price

Server transaction:

text
verify catalog version and server-side price lock/update wallet create inventory item insert purchase transaction insert outbox event commit

The request_id must have a unique constraint so retries cannot charge twice.

Never accept price, rarity, item stats or ownership from the client.


14. Friends, ignore and clans

PostgreSQL authority

Possible tables:

sql
friend_requests ( request_id, sender_id, receiver_id, status, created_at ); friendships ( lower_user_id, higher_user_id, created_at, PRIMARY KEY (lower_user_id, higher_user_id) ); ignore_edges ( owner_user_id, ignored_user_id, created_at, PRIMARY KEY (owner_user_id, ignored_user_id) ); clans ( clan_id, name, owner_user_id, version ); clan_members ( clan_id, user_id, role, joined_at, PRIMARY KEY (clan_id, user_id) );

Local caches

When a user connects to chat:

text
load their ignore list store it in an unordered_set<UserId>

On ignore-list modification:

text
1. Commit PostgreSQL. 2. Publish IgnoreListChanged { user_id, version }. 3. The chat node hosting that user reloads or applies the delta.

Do not query PostgreSQL or Redis for ignore status on each message.

Friend online-state notifications should be derived from presence events, not from polling.


15. Create-game and waiting-room architecture

Room owner

Each room has one authoritative owner instance:

text
room_id → lobby/room node

Local actor:

cpp
struct Room { RoomId room_id; UserId creator_id; RoomState state; std::uint16_t social_map_id; std::uint16_t battle_map_id; std::uint8_t capacity; std::vector<RoomMember> members; std::uint64_t version; std::uint64_t fencing_token; };

Redis snapshot

text
room:{room_id}:state STRING packed Room room:{room_id}:owner STRING {node_id, fencing_token} EX 15 room-index:{eu:lost_beach}:open ZSET score = creation_time member = room_id

The room actor serializes joins. It checks capacity in RAM and snapshots changes to Redis.

Do not allow every lobby instance to independently execute:

text
SCARD room-members SADD room-members user

Two simultaneous joins can otherwise overbook the last slot.

Join flow

text
1. Client sends JoinRoom(room_id). 2. Receiving lobby resolves room owner. 3. Request routes to room-owner node. 4. Room actor checks: - room is Open; - player state is Idle; - player is not already in another room; - room has capacity; - level/rank/password restrictions. 5. Player state becomes WaitingRoom. 6. Room member is added. 7. RoomChanged is broadcast to interested lobby nodes.

Start flow

text
Open Starting Reserve all member runtime states Request game-server allocation Create game instance Mint one game ticket per player Send assignments InGame

If allocation fails:

text
Starting → Open release player reservations

Use a unique start operation ID so retries cannot create two game instances.


16. Matchmaking architecture

Your matchmaking server should not tell the lobby server to construct and start a game.

Use:

text
Lobby/Frontend ↓ create ticket Matchmaking ↓ match proposal Match Director ↓ allocate Game Allocator Game Server ↓ assignment/tickets Lobby/Gateway notifies clients

This is also the pattern used by Open Match: the frontend creates tickets; the match function proposes matches; a Director fetches matches, obtains dedicated game-server allocations and sets player assignments. (Open Match)

Agones is an available option for maintaining and allocating warm dedicated-game-server fleets on Kubernetes, but it is not mandatory for your C++ backend. (Agones)

Redis queue design for your own matchmaker

text
mm-ticket:{ticket_id} HASH user_id party_id region mode rating map_preferences enqueue_ms version EX 900 mm-user:{user_id} STRING ticket_id EX 900 mm:{eu:ranked_2v2}:queue ZSET score = enqueue_ms member = ticket_id

Additional rating indexes:

text
mm:{eu:ranked_2v2}:rating ZSET score = rating member = ticket_id

All queue-related keys for a mode/region should use the same Redis hash tag where an atomic reservation is required.

Matchmaker RAM

text
ticket_id → ticket rating bucket → ordered ticket collection party_id → party user_id → active ticket reserved ticket IDs

Redis supports restart recovery; the local indexes provide fast matching.

When candidates are selected:

text
1. Mark candidates Reserved with reservation ID and TTL. 2. Confirm none have cancelled or changed state. 3. Produce MatchFound. 4. Remove from queue. 5. Ask allocator for a server.

Every operation must be idempotent.


17. Local RAM design by service

auth_server

text
No persistent player state Argon2 worker pool small local abuse counters as optional front cache Redis/PostgreSQL connection pools

lobby_server

text
connection_id → LobbyConnection user_id → connection_id local connected-user state cache rooms owned by this instance room subscriptions pending request IDs friend/clan summary caches for local users bounded outbound queues

Lobby-state caches are for display and quick rejection, not final authorization.

chat_server

text
connection_id → ChatConnection user_id → connection_id map_id → local connected users map_id → local eligible recipients user_id → ignore set sender_id → local recipients ignoring sender target route cache recent message ID deduplication rate-limit buckets bounded outbound queues

presence_server

text
connection_id → PresenceConnection user_id → connection_id user_id → PlayerRuntimeActor map shard → MapActor map shard → locally connected edge nodes per-map sequence number short delta replay ring buffer ownership leases and fencing tokens

matchmaking_server

text
ticket_id → MatchTicket user_id → ticket_id party_id → party queue/mode/rating indexes reservation_id → candidates recently completed operation IDs

game_server

text
game_id → GameInstance user_id → game_id expected players connected players turn/action sequence reconnect slots game-level outbound buffers periodic snapshots or action log

Game state should be authoritative in the GameInstance, not Redis. Redis may hold server assignment and recovery metadata.


18. Redis Pub/Sub, Streams and broker rules

Use Pub/Sub for

text
map chat trade-chat broadcast typing indicators noncritical presence hints live room-list changes cache invalidation hints

Redis Pub/Sub messages are discarded after delivery and missed while subscribers are disconnected. Sharded Pub/Sub is preferable on Redis Cluster for high-volume subjects. (Redis)

Use Redis Streams or JetStream for

text
friend accepted clan membership changed purchase completed trade completed game result submitted notification created audit/moderation processing reliable retryable jobs

Redis Streams are append-only logs with IDs and consumer-group support. (Redis)

Be careful: a single consumer group distributes messages between consumers. It does not broadcast every event to every chat node. For realtime broadcast, use Pub/Sub/NATS plus snapshot/version recovery.

Never use keyspace notifications for correctness

Keyspace notifications use Pub/Sub, can be missed, and in a Redis Cluster are node-specific rather than automatically broadcast from every node. (Redis)

Do not depend on an expiry notification to mark a player offline. Use leases and explicit expiry checks.


19. Redis Cluster durability warning

Redis Cluster replication is asynchronous and can lose acknowledged writes during some failovers. (Redis)

Therefore:

Safe consequences of Redis loss:

text
player must reconnect room must be rebuilt presence temporarily disappears matchmaking ticket must be recreated chat message is lost

Unacceptable consequences:

text
inventory item disappears currency returns after purchase trade commits twice game reward duplicates clan ownership changes

The second category must remain PostgreSQL-authoritative.


20. Exact handling of your two multi-instance examples

A. Player 1 sends a DM to player 5 on another instance

text
Player 1 chat_server1 ↓ GET route:chat:{5} Redis: player 5 is on chat_server2, epoch 44 ↓ publish chat.node.chat_server2 chat_server2 ↓ verify epoch + ignore list Player 5

Redis operations:

text
Usually zero due to route cache At most one GET on route-cache miss One broker publish

No global player scan.

B. Player 1 sends “hi all” to Lost Beach

text
Player 1 chat_server1 ↓ one publish to chat.map.eu.lost_beach.0 ├──────── chat_server1 → local players 1,2,3 └──────── chat_server2 → local players 4,5,6

Each node already knows its local users’ map and activity from presence-state events.

Redis operations per chat message:

text
zero state lookups zero roster scans zero per-recipient route lookups

That is the architecture you want.


21. Practical C++ networking recommendations

For lobby/chat/presence traffic:

text
TCP + TLS 1.3 length-prefixed binary framing Protobuf or a carefully versioned custom schema request_id for request/response correlation message_id for deduplication protocol version negotiation strict maximum frame size bounded send queues timeouts and heartbeats

A useful header:

cpp
struct FrameHeader { std::uint32_t payload_size; std::uint16_t protocol_version; std::uint16_t message_type; std::uint64_t request_id; std::uint64_t sequence; };

Do not add your own message HMAC when TLS already provides transport integrity, unless you have a separate protocol-level requirement.

Use Asio strands or actor ownership so each connection or room actor is mutated by one execution context. Avoid a single global mutex around all sockets or all maps.

For broadcast, encode the payload once:

cpp
auto packet = std::make_shared<const ByteBuffer>(encode(message)); for (ConnectionId id : recipients) { connections[id]->enqueue(packet); }

Do not serialize the same message 1,000 times.


22. Final architecture decisions

I would implement your backend with these decisions:

  1. Keep a stable 64-bit user_id; optionally expose a different opaque public player ID.

  2. Replace one status enum with connectivity + activity + map_id.

  3. Make presence/player-state the sole authority for runtime-state transitions.

  4. Keep PostgreSQL authoritative for inventory, currency, social relationships, clans, trades and results.

  5. Keep Redis authoritative only for recoverable ephemeral coordination.

  6. Use local RAM for every hot-path socket, membership and fan-out operation.

  7. Route DMs using user_id → chat node leases.

  8. Route map chat with one map-subject publish; every chat node fans out only to its local recipients.

  9. Send initial roster snapshots with sequence numbers, then batched deltas.

  10. Put game chat in the game server.

  11. Use a game allocator/director between matchmaking and game servers.

  12. Make trades appear peer-to-peer while remaining fully server-authoritative.

  13. Use Redis Pub/Sub only for disposable realtime delivery; use Streams/JetStream plus durable state for important events.

  14. Prefer a single TLS edge connection instead of three client-facing persistent connections.

  15. Treat the device fingerprint as a risk signal, not as a security secret.

This design comfortably handles the 1,000–2,000-player social-map sizes you described and scales horizontally without requiring Redis access for each chat recipient or every UI update.

Compartilhar esta Q&A