Run exactly one application instance per install. Multi-node gameplay isn’t supported today, on Docker Compose or Kubernetes. Scale by giving that one instance more CPU and memory.
The codebase contains a Redis adapter and a multi-node compose file, which can make it look otherwise. This page explains what they do, and why running more than one node is unsafe, including for player funds.
What’s Supported
| Setup | Status |
|---|---|
| One app instance with more CPU and memory | Supported. This is the way to scale |
| Managed PostgreSQL and Redis with one app pod on Kubernetes | Supported |
Planned single-instance redeploy with table snapshots (LIVE_MIGRATION_ENABLED=true) | Supported |
| Two or more app instances serving gameplay | Not supported |
docker compose --scale app=N | Doesn’t work. docker-compose.prod.yml sets a fixed container_name, and the bundled nginx has a single upstream, app:3000 |
Kubernetes replicaCount > 1 or autoscaling.enabled | Blocked by the chart |
| Separate REST, WebSocket and worker processes | Not supported. One process runs all of them, and there’s no setting to split them |
| PostgreSQL read replicas | Not supported. The app connects through a single DATABASE_URL |
Why Multiple Nodes Are Unsafe
Adding a second node doesn’t share the load. It creates a second, disconnected game server on the same database.
Table state is in process memory. Live tables, seat assignments and spectators are held in in-memory maps inside the app process. They aren’t stored in Redis or the database during a hand, and nothing routes a request to the node holding a given table.
Each node runs its own copy of every table. At startup, each process loads all non-paused tables from the database into its own memory. Two nodes would run independent games for the same table, with separate seats, cards and timers.
A second node can refund seated players. At startup, the app runs crash recovery on open cash table escrows. It refunds any player it doesn’t see seated, and it only checks its own memory. A node starting up can’t see players seated on another node, so it would pay out their stacks to their wallets while they’re still playing. This is a direct financial risk on real-money tables.
Background jobs aren’t coordinated. The game scheduler, bot scheduler, settlement cron, fraud sweep and tournament monitor each run on timers inside the process. There’s no leader election or cluster lock, so every node runs its own copy.
Redis Adapter
docker-compose.prod.yml always sets REDIS_ENABLED=true, and Helm installs can point at Redis too, so the adapter is normally active even on a single node. When REDIS_ENABLED=true and REDIS_URL is set, the app attaches the Socket.IO Redis adapter, which relays broadcasts between processes.
It relays messages. It doesn’t share game state. A broadcast can reach a socket on another node, but that node still has no copy of the table to act on.
If Redis can’t be reached at startup, the app logs an error and falls back to the in-memory adapter.
What Else Uses Redis
| Feature | If Redis is enabled but unavailable |
|---|---|
| Lobby and table caching | Falls back to the database |
| 2FA attempt tracking | 2FA checks fail with TWO_FACTOR_ATTEMPT_STORAGE_UNAVAILABLE (HTTP 503) |
| Developer API request-ID replay protection | Reported as unavailable rather than skipped |
| Wallet aggregator replay protection | Reported as unavailable rather than skipped |
| Payment webhook rate limits | Webhooks get HTTP 503, so providers need to retry them |
So Redis is a dependency worth monitoring even on one node: an outage blocks 2FA logins and payment webhooks.
Sticky Sessions
Sticky sessions pin a client to one node so that Socket.IO’s handshake and long-polling requests all reach the same process.
- Docker Compose: not needed. There’s one app container behind one nginx upstream.
- Helm: the default ingress annotations set cookie affinity (
nginx.ingress.kubernetes.io/affinity: cookie, with the cookiepe-affinity). They’re safe to keep, and do nothing useful with one replica.
Sticky sessions don’t make multi-node safe. Two players at the same table could be pinned to different nodes, and each node would run its own game. None of the problems above are solved by affinity.
Table Storage
Tables live in a Map in the app process. This keeps state access fast, and it has these consequences:
- Capacity is limited by the size of one process. Watch memory and event-loop lag as your table count grows. See Observability.
- A restart interrupts live tables unless live migration is enabled. Without it, interrupted hands are void, and players who are no longer seated are refunded as described in Cash Table Funds.
- Live migration is for one instance only. With
LIVE_MIGRATION_ENABLED=true, supported upgrade paths drain active hands and write table snapshots to the database, and the new process restores them. It doesn’t let two instances share tables. See Kubernetes & Helm and Upgrades and Rollback.
Redis-backed table storage doesn’t exist in the product today.
The Kubernetes Guard
The Helm chart refuses to render with replicaCount > 1 or autoscaling.enabled: true, unless you also set:
gameState:
multiReplicaAcknowledged: true
This setting removes the chart’s check and nothing else. It doesn’t make extra replicas safe for gameplay, and the application has no mode that turns off gameplay, schedulers or crash recovery on a replica. All the risks above still apply, so leave it false.
The Multi-Node Compose File
docker-compose.multinode.yml stands up two app nodes, Redis and an nginx load balancer for benchmarking the Redis adapter. It isn’t a deployment option:
- It’s marked for benchmarks and local use only, and isn’t included in the release tarballs.
- It has no license settings or TLS, and uses a separate
poker_loadtestdatabase. - Its nginx configuration is tuned to a query parameter sent by the load-test harness.
The harness’s multi-node mode (--multi-node, used with --simulate) models cross-node traffic rather than measuring it. Its results are simulated, and don’t show that multi-node play works.
How to Scale Today
- Add CPU and memory to the app. On Compose, raise the
applimits indocker-compose.prod.yml. On Helm, raise the app’s resources. - Size the connection pool. Set
DATABASE_POOL_SIZEandDATABASE_POOL_TIMEOUT. See Database Pooling. - Move PostgreSQL and Redis to managed services on Kubernetes. See Kubernetes & Helm.
- Measure your own install. We don’t publish a certified player or table capacity figure, so base your sizing on the metrics from your own traffic and hardware.