Scaling & Multi-Node

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

SetupStatus
One app instance with more CPU and memorySupported. This is the way to scale
Managed PostgreSQL and Redis with one app pod on KubernetesSupported
Planned single-instance redeploy with table snapshots (LIVE_MIGRATION_ENABLED=true)Supported
Two or more app instances serving gameplayNot supported
docker compose --scale app=NDoesn’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.enabledBlocked by the chart
Separate REST, WebSocket and worker processesNot supported. One process runs all of them, and there’s no setting to split them
PostgreSQL read replicasNot 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

FeatureIf Redis is enabled but unavailable
Lobby and table cachingFalls back to the database
2FA attempt tracking2FA checks fail with TWO_FACTOR_ATTEMPT_STORAGE_UNAVAILABLE (HTTP 503)
Developer API request-ID replay protectionReported as unavailable rather than skipped
Wallet aggregator replay protectionReported as unavailable rather than skipped
Payment webhook rate limitsWebhooks 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 cookie pe-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_loadtest database.
  • 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

  1. Add CPU and memory to the app. On Compose, raise the app limits in docker-compose.prod.yml. On Helm, raise the app’s resources.
  2. Size the connection pool. Set DATABASE_POOL_SIZE and DATABASE_POOL_TIMEOUT. See Database Pooling.
  3. Move PostgreSQL and Redis to managed services on Kubernetes. See Kubernetes & Helm.
  4. 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.
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.