Why 50,000 concurrent is a different problem, not a bigger one
A backend for 500 concurrent players and a backend for 50,000 are not the same system scaled by 100. Below roughly 2,000 CCU almost any reasonable design works — a single application server with an in-process session map and a relational database will hold it, and most of the engineering effort goes into gameplay features rather than infrastructure.
Somewhere between 5,000 and 10,000 the assumptions start failing one at a time, and they fail in a specific order. Knowing that order is most of the value of having done it before, because each failure is cheap to design around in advance and expensive to retrofit under live traffic.
Concretely, what changes:
- State stops fitting in one process. The in-memory session map that made everything simple becomes the thing that prevents you from running two instances.
- The database stops being a database and becomes a queue. Write contention on a hot table — leaderboards, inventory, match results — serialises everything behind it.
- Connection count, not CPU, becomes the scaling signal. A box holding 8,000 idle websockets can look 15% busy right up to the moment it falls over.
- Deploys stop being free. At 500 players a rolling restart is invisible. At 50,000 it is a support incident unless sessions can survive it.
What the architecture actually looks like
The stack below is the one behind the numbers in the first paragraph. It is deliberately unexotic: every part is a well-understood component doing one job, because a game backend is operated by a live-ops team at three in the morning and cleverness is a liability at that hour.
1. Stateless edge — connection termination
Player connections terminate on a horizontally scalable tier that holds no game state at all. Its only jobs are transport, authentication, and routing messages onward. Because it is stateless it can scale purely on connection count and can be replaced instance by instance without anyone noticing.
The discipline here is negative: the moment the edge tier starts caching "just a little" player state to save a hop, you have re-created the single-process problem across N boxes, and now it is a distributed consistency bug instead of a simple one.
2. Authoritative session services — one writer per piece of state
Game state lives in services that own it exclusively. A match, a session, a player inventory: each has exactly one authority, and every mutation goes through it. This is the single most important decision in the whole design, and it is the one most often compromised under deadline pressure.
The payoff is that the hard problems — duplicate item grants, rubber-banding, two clients disagreeing about who won — become impossible by construction rather than being fixed with locks and retries after they show up in production. We build these in Go, with gRPC between services: goroutine-per-session is a natural fit for tens of thousands of concurrent sessions, and the garbage collector's latency profile is predictable enough to sit in a gameplay path.
3. A Redis-backed event fabric
Between the tiers sits Redis, doing pub/sub for fan-out and queue patterns for work that must not be lost — matchmaking coordination, cross-service notification, presence. Redis is chosen here for latency floor rather than features: sub-millisecond at the p99 in-region, with an operational model any on-call engineer already understands.
A common and expensive mistake is putting a general-purpose message broker in the gameplay path because it appeared on an architecture diagram. Durability guarantees cost latency. Use the durable path for things that must survive a crash — purchases, match results, progression — and the fast path for things that are stale in 50 ms anyway, like position updates.
4. Autoscaling on the right signal
The runtime is Kubernetes, with scaling rules driven by connection count and matchmaking queue depth rather than CPU utilisation. This is the correction almost every game backend needs at some point: CPU is a lagging indicator for a connection-bound workload, and by the time it moves, the queue is already deep enough that players are feeling it.
Observability is Grafana on metrics the live-ops team chose, not the ones that were easy to emit. The test of a dashboard is whether someone can answer "is the game healthy right now" in under ten seconds without asking an engineer.
What breaks first, in order
| Roughly when | What breaks | What it costs to fix later |
|---|---|---|
| 2k–5k CCU | In-process session state prevents horizontal scaling | High — it is a re-architecture of the service boundary, not a change |
| 5k–10k CCU | Write contention on hot tables (leaderboards, inventory, match results) | Medium — usually solved by moving the hot path off the relational store |
| 10k–20k CCU | CPU-based autoscaling reacts too late for connection-bound load | Low — a scaling policy change, if the metrics already exist |
| 20k+ CCU | Deploys become player-visible; no session drain, no graceful handover | Medium — needs connection draining and version-tolerant protocol |
| Any scale, event spikes | Cold-start latency on scale-out during a burst | Low — pre-warming and headroom, once someone has measured it |
How do you know it actually holds?
You load-test continuously during delivery, not in the final week. This is the practice that separates backends that hit their CCU target from backends that were designed to.
On the project behind these numbers, load simulation ran throughout the build rather than as a release gate, which is why the first real traffic peak was uneventful. The sequence that worked:
- Map the domain first. Separate gameplay-critical paths from live-ops paths, so the architecture can prioritise the ones players feel.
- Build one vertical slice. Matchmaking and session orchestration end to end, before widening into adjacent concerns.
- Simulate load from that point on. Every sprint, against the real protocol, at a multiple of the target.
- Roll out observability before you need it. Dashboards and alerts handed to the live-ops team well before launch, so they are familiar rather than novel during the first peak.
- Harden the deploy path. Autoscaling and release controls tuned so a patch during an event is routine.
A target of "50,000 CCU" is also worth interrogating before you design for it. Peak concurrency is what the infrastructure must survive, and it is frequently confused with monthly actives or registered users — which are unrelated numbers, often two orders of magnitude apart. Designing for a peak you will not reach for two years is the most common way to overspend on a game backend; see what drives the number for the rest of that list.
What this does not cover
Deliberately out of scope above, because each is its own decision: authoritative physics and rollback netcode (a client-engine concern more than a backend one — see Unity or Unreal for a multiplayer game), anti-cheat, regional sharding and cross-region matchmaking, and on-chain asset ownership if the title has any (what actually belongs on-chain).
The full delivery story for the numbers quoted here — client context, delivery scope, and measured outcome — is in the multiplayer game backend case study.