What Breaks Game Backends at Massive Scale

In July 2021, 1047 Games had the kind of problem most teams say they want. Their arena shooter Splitgate had gone free-to-play with cross-play across PC, PlayStation, and Xbox, and the concurrent player count went from a couple hundred during its early-access to over two million downloads in about two weeks. Then the backend hit a wall.
The wall was oddly specific. After a night of optimizing and a call with AWS, 1047 posted that their database, Redis, could only handle 65,536 concurrent players. It wasn't a shortage of game servers, and it wasn't a lack of CPU. It was a 16-bit ceiling sitting inside one data-layer dependency, and no amount of extra compute anywhere else would move it. They built a login queue to admit players up to the limit while they reworked the database, and they took the beta offline more than once to do it.
The players' response was the obvious one: buy more servers. The studio's server engineer went on TikTok to explain why that missed the point, comparing it to opening a restaurant and being told to add tables when the real constraint is the kitchen. Their own public summary was that the playerbase was growing faster than they could add capacity, and the fix was going to be real re-architecture, not a bigger invoice.
That's what massive-scale backend failure usually looks like. You don't find one giant "we need more servers" problem. You find the next bottleneck every time the system crosses a threshold it was never shaped for, and it's rarely the thing players are shouting about. 1047 eventually migrated Splitgate to AccelByte and pushed well past 100,000 concurrent, but the useful part is the shape of the failure, not who fixed it.
This piece is about that shape. What actually breaks when a backend that's fine at 10,000 players has to hold 100,000 or a million, why horizontal scaling doesn't automatically save you, and how to find your own ceilings before launch traffic does.
Massive Scale Is a Workload Shape, Not a CCU Number
"Massive scale" is usually stated as a CCU target: a million concurrent users, say. But CCU on its own tells you almost nothing about what the backend has to do. Take two games, for example:
-
Game A has one million connected players. They spend most of their time in ten-player matches, start a new matchmaking cycle every twelve minutes, and touch relatively little persistent state.
-
Game B has 250,000 connected players, but it is a persistent world with frequent inventory writes, guild presence, chat fan-out, world-state updates, progression, and economy transactions.
Game B can easily put more pressure on parts of the backend than Game A.
The player count tells you how many people are there. It does not tell you what they are doing. For launch planning, the useful numbers are the rates behind the player behavior:
-
logins per second
-
long-lived connections
-
party creates, joins, and leaves
-
matchmaking tickets per second
-
sessions created per second
-
reads and writes by data domain
-
inventory and economy transactions
-
chat messages and recipient fan-out
-
dedicated-server claims
-
telemetry events
-
external platform calls
This is also why our own one-million-CCU matchmaking test starts by defining a player flow rather than treating one million as sufficient evidence. A scale number without the workload behind it tells you very little about your game.
For any player journey, the practical ceiling is the first dependency in that path that saturates. That is why "we scaled the servers" is an incomplete sentence.
Scaled which part, for which player journey, against which shared dependency?
The First Bottleneck Is Usually the Front Door
Big launches almost never ramp smoothly. A patch unlocks at a scheduled time. Maintenance ends. A streamer goes live. A storefront feature lands. Tens of thousands of clients can attempt the same operation within seconds.
Login is particularly sensitive because one login can trigger much more than one lightweight request:
-
platform authentication
-
token validation
-
account lookup or creation
-
legal or consent checks
-
connection setup
-
profile initialization
-
downstream requests for whatever the game chooses to load immediately
Two different numbers matter here:
-
Concurrent capacity:
how many players can remain connected at once. -
Admission rate:
how many new players the system can safely bring through the front door per second.
A backend can comfortably hold hundreds of thousands of established sessions and still collapse if too many new sessions try to initialize simultaneously. That is what admission control is for.
AccelByte Login Queue is one implementation of this pattern. It can trigger from both maximum concurrency and maximum login rate, with a safety margin that activates the queue before the concurrency ceiling. Queued players get a position and estimated wait time instead of repeatedly hammering the rest of the login flow.
The broader principle matters more than the product: when arrivals synchronize, deliberately slowing admission can preserve the rest of the system.
A login queue is not evidence that a backend failed to scale. It is a mechanism for deciding how fast the backend accepts new work when demand is faster than downstream systems can safely initialize it.
Autoscaling Is Reactive; Player Demand Is Not
"Autoscaling will handle it" is true over a long enough time window. The problem is the window before the new capacity is ready.
Capacity has to be scheduled, started, initialized, registered, warmed, and declared healthy. If the workload needs containers, VMs, caches, database connections, or game-server images, those steps take real time.
Dedicated servers make the delay obvious. Starting a new dedicated server may take 1 to 10 minutes depending on the path including initializing a host, downloading the DS image, starting the process, and connecting it to AMS.
A player who just found a match does not care that the fleet will be correctly sized ten minutes from now. That is why production fleets keep Ready capacity around.
A rough way to think about it is:
ready buffer ≈ expected server demand during the provisioning lag
The exact buffer is game-specific, but the engineering question is always the same: how many claims can arrive before new capacity catches up?
There is a regional version of the same problem. You can have enough global capacity and still have players waiting because the Ready servers are in the wrong region. A launch forecast can say 40% North America, 35% Europe, 25% Asia-Pacific and turn into something completely different because of a creator, launch timing, or local storefront placement.
Our own multiplayer servers scale fleets independently by region for exactly this reason. "We have enough servers" and "this player can get a server in the right region right now" are different claims.
Databases Break Because Traffic Is Uneven, Not Because It Is Large
A database can have plenty of total throughput and still throttle badly.
The classic failure is uneven access.
A key such as player:{playerId} usually distributes naturally because traffic is spread across many players.
A badly modeled shared object such as leaderboard:global, or event:currentSeason can do the opposite.
If every player updates the same record, you have created one hot key no matter how much capacity exists elsewhere. AWS documents this directly for DynamoDB: partition-key design needs to distribute activity, and one partition has finite read/write capacity even if the table as a whole has unused throughput. The exact mechanics differ by database but the architectural problem does not.
Games are good at creating concentration unexpectedly. A LiveOps event can take traffic that was spread across thousands of quests and point it at one shared objective. The same can happen with:
-
a global event accumulator
-
a tournament bracket
-
one extremely large guild
-
limited global stock
-
a global counter
-
a shared world-state record
-
low-cardinality indexes
-
account-level locks during cross-region operations
This is also where "just add API pods" stops helping. Stateless application work is comparatively easy to spread. Shared state, locks, ordering, queues, and coordination are where horizontal scaling gets harder. If one operation needs a globally serialized decision, another stateless instance does not remove that serialization point.
You do not need to eliminate all coordination. You need to know where it exists.
Caches Can Hide the Real Ceiling
A cache can make a weak database path look healthy right up until the cache stops protecting it. Then a deploy clears entries, a set of TTLs expires together, a node fails, or a region fails over.
Thousands of requests that used to be cache hits become database reads at the same time. That is the cache-stampede version of the same synchronized-arrival problem. Common defenses include:
-
TTL jitter
-
request coalescing
-
stale-while-revalidate behavior
-
cache warming
-
bounded refresh concurrency
The uncomfortable test is simple: if this cache misses under load, does the layer behind it survive? If the answer is no, the cache is not only an optimization. It is hiding a hard dependency.
Failure Can Generate More Traffic
Everything so far is about capacity that isn't where you need it. The next category is worse, because the failure feeds itself.
Picture a downstream service that starts responding slowly. An upstream service retries. The client SDK also retries. A gateway has its own retry behavior. Suddenly one player action is generating many backend attempts because the backend is already unhealthy. The feedback loop looks like: service slows → calls time out → callers retry → load rises → service slows further
AWS's current Well-Architected guidance recommends bounded retries, exponential backoff, jitter, and choosing an appropriate layer for retry logic instead of blindly retrying at every layer. There is another half to the problem: what are you retrying?
A read can often be repeated safely. A mutation such as grantReward(eventId=abc123) needs different semantics. If the first request succeeded and the response was lost, the retry must not grant the reward again.
That is where idempotency comes in. An idempotency key does not mean "retry aggressively." It means that when a retry policy says an operation should be repeated, repeating it does not duplicate the side effect. That matters for:
-
purchases
-
currency
-
item grants
-
progression rewards
-
entitlement writes
-
match-result commits
At scale, resilience and economy correctness are often the same design problem.
One Player Event Is Many Backend Operations
Not every multiplier comes from failure. A single match completion may trigger:
-
result commit
-
player-stat updates
-
XP
-
achievements
-
leaderboard updates
-
rewards
-
battle-pass progression
-
quest progression
-
telemetry
-
notifications
For a ten-player match, several of those are per-player writes.
This is why connection-only load tests are misleading.
Suppose a game has 100,000 concurrent matches with a ten-minute average match duration. That is roughly 167 match completions per second at steady state.
If, purely as an illustration, each completion results in 50 persisted or queued backend operations, the result path alone is producing more than 8,000 downstream operations per second. The number 50 is not the point. The multiplication is.
This is also where synchronous design decisions become visible. If the match cannot complete until stats, achievements, rewards, leaderboard updates, telemetry, quests, and notifications all finish, the player is waiting on the slowest dependency in that list. Where game semantics allow it, keep the authoritative work on the critical path and move derived or nonessential work off it.
That might mean:
-
Synchronous
-
commit match result
-
grant the reward that must be immediately correct
-
-
Asynchronous
-
analytics
-
notification delivery
-
secondary indexes
-
derived statistics
-
The rule is not "everything should be async." The rule is: make the critical path no longer than the game's correctness requirements demand.
Queues Absorb Bursts, but They Do Not Create Capacity
Once work becomes asynchronous, queues become useful and dangerous. A queue can smooth a short burst. It cannot fix a permanent rate mismatch.
If producers add 10,000 jobs per second and consumers drain 6,000, the backlog grows by 4,000 per second. After ten minutes, you have 2.4 million jobs waiting.
Traffic can return to normal and the system can still be unhealthy because it is now working through old load. So queue health needs more than depth.
For every important queue, know:
-
maximum depth
-
oldest-message age
-
producer rate
-
consumer rate
-
expected catch-up time
-
retry/dead-letter behavior
-
which work can be dropped
-
which work has priority
An unbounded queue is an outage with a longer fuse.
Backpressure Is the Decision to Stop Accepting Work You Cannot Finish
When capacity is exhausted, a healthy system needs a deliberate response. Based on the workload, that can mean:
-
429 / throttle
-
reject
-
queue
-
delay admission
-
drop optional work
-
reduce update frequency
-
disable a nonessential feature
Games usually have work that matters at very different levels. An active session or money-changing transaction may deserve very different protection from a cosmetic recommendation refresh or high-detail analytics.
Do not invent that priority during the incident. Write what can degrade before launch.
Blast Radius Matters as Much as Raw Capacity
A backend can have enough total capacity and still fail badly because its dependencies are coupled too tightly. Imagine login does this synchronously:
Identity → profile → inventory → entitlements → friends → presence
and every step must succeed before the player gets through. Inventory becomes unhealthy. Now login becomes unhealthy.
Clients retry login, so identity, profile, entitlements, friends, and presence all receive more work too, even though none of them caused the incident. That is how one overloaded service becomes a platform outage.
For each workflow, classify dependencies honestly:
-
required now
-
optional
-
async
-
lazy-loaded
-
cached
-
degradable
Maybe identity is required for login while friends can appear later. Maybe analytics can drop or buffer. Maybe a leaderboard can be stale for a few minutes. Maybe a premium-currency purchase must fail because correctness is more important than availability. Those are domain decisions, not platform-wide rules.
The usual tools are familiar:
-
timeouts
-
circuit breakers
-
bulkheads
-
async boundaries
-
fallbacks
-
dependency-specific budgets
-
graceful degradation
What matters is the failure boundary they create.
Third-Party Quotas Are Part of Your Scale Limit
Platform identity, storefront APIs, payment validation, anti-cheat, moderation, and voice services all have their own capacity and failure behavior.
If your backend handles 100,000 requests per second but an external entitlement path starts throttling at a much lower rate, that external dependency is part of the player's effective ceiling. The right question for every external call is:
If this provider slows down, which player journeys have to slow down with it?
That forces a much more useful design conversation than "is this API reliable?"
Matchmaking Can Look Healthy While Players Still Cannot Play
Matchmaking does not scale like a basic CRUD endpoint. Its cost depends on the shape of the search:
-
active tickets
-
party sizes
-
skill attributes
-
latency constraints
-
crossplay constraints
-
team composition
-
pool design
-
flex rules
-
backfill
-
worker distribution
More players can even make some matching problems easier because there are more candidates. What matters is the ruleset and the pool, not only tickets per second. But there is a second failure that teams miss: a match found is not a playable match.
The player journey is closer to:
Ticket → match found → session created → server claimed → connection info → player connects
Matchmaking can report healthy p99 latency while session creation is throttling or there is no Ready server in the correct region.
Our one-million-CCU matchmaking test is useful here because the workload is published. The test:
-
connected simulated users until reaching one million CCU
-
held one million connected users for one hour
-
used different party sizes
-
used a single 5v5 match pool
-
matched on MMR and latency with flexing rules
-
simulated 8 to 13 minutes of "playing" before players re-queued
-
reached close to 100,000 concurrent active match sessions
-
kept p99 successful matchmaking time under 35 seconds
The single pool matters because our write-up explicitly describes it as a worst case for the Matchmaking service. The limitation matters just as much: this test exercised Lobby, Party, Matchmaking, and Session. The ruleset had dedicated-server readiness disabled. It simulated the play interval rather than proving that AMS could allocate a corresponding dedicated server for every simulated match. That is how scale evidence should be read.
Not:
"They tested a million, therefore my game is covered."
But:
"What exact player behavior and services were exercised, what result did they get, and what was outside the test?"
That is the same standard we recommend in Evaluating Game Backends: Ten Questions to Ask Every Vendor.
Find Your Ceilings Before Your Players Do
None of this is findable without observability, and observability has a cruel property: it tends to fail exactly when you need it most.
Once the backend is under pressure, it often emits more telemetry and it's exactly when your telemetry pipeline is also under the most pressure. Common failure modes include:
-
log ingestion throttling
-
high-cardinality metrics blowing up
-
dashboards lagging
-
traces being sampled so aggressively that the interesting requests disappear
Treat observability as a workload with its own capacity and degradation policy.
Some useful practices are:
-
bounded logging
-
cardinality controls
-
sampling rules
-
request correlation
-
high-value SLO metrics
-
priority treatment for incident signals
The other trap is the average. "Average latency is 120 ms" can hide a terrible tail. At a large population, 1% is not small. At one million players, 1% is 10,000 people.
Watch distributions and outcomes:
-
p50 / p95 / p99 where appropriate
-
timeout rate
-
error rate
-
throttle rate
-
queue age
-
end-to-end player-journey success
A player does not care that Matchmaking is green if server allocation is red three steps later.
The weak load test is: one million users call /health. A slightly less weak load test is: one million users authenticate
A useful load test reproduces the behavior that creates the real work. For a session-based multiplayer game, that may be:
Authenticate → connect → form party → submit ticket → form session → simulate play duration → commit progression/results → re-queue → repeat
Then test the ugly traffic shapes separately:
-
instant launch spike
-
one-region spike
-
hot-key LiveOps event
-
concentration into one matchmaking pool
-
reconnect storm
-
cache cold start
-
downstream 429s
-
database throttling
-
slow server provisioning
-
partial region failure
-
deploy at peak
And do not only ask whether the backend survives expected load. Ask whether it survives expected load while something is broken. Inject:
-
latency
-
timeouts
-
dropped connections
-
throttling
-
dead consumers
-
dead dependencies
-
regional loss
Then inspect what retries, queues, failovers, and clients do.
Recovery Is Another Load Spike
Recovery is its own traffic scenario, and often a worse one than the incident that caused it so it deserves explicit testing. A service goes down and 200,000 clients disconnect. The service comes back. Now 200,000 clients want to:
-
reconnect
-
reauthenticate
-
restore subscriptions
-
reload state
-
replay failed operations
The recovery wave can be larger than the event that caused the outage. Use:
-
jittered reconnect
-
staged reopening
-
admission control
-
rate-limited replay
and load-test recovery just like launch.
Watch What Bends Before It Breaks
The best output from a scale test is not "Passed", it's "At this workload, database write latency starts climbing while the rest of the path still looks healthy."
That is a useful finding because it tells you what to fix next. Scaling work is iterative:
load test → find ceiling → fix / redesign → load test again → find next ceiling
Finding another bottleneck after fixing the previous one is not evidence that the architecture is bad. It is how capacity discovery works. The dangerous result is believing the first successful test means the system now scales indefinitely.
What “Massive-Scale Ready” Actually Means
Here's the whole failure landscape as a reference, roughly in the order a launch tends to meet it.
"More servers" answers one question:
Do we have enough raw compute at this layer?
Massive-scale failures usually come from more specific questions:
-
Does traffic arrive faster than the system can admit it?
-
Is the load distributed across state?
-
Which operation is serialized?
-
How much downstream work does one player event create?
-
Do retries amplify failure?
-
Can queues actually drain?
-
Which services fail together?
-
Is capacity in the right region?
-
Can we see the first resource starting to bend?
-
What happens when everyone reconnects at once?
That is what "massive-scale ready" should mean. Not "we passed one million CCU."
It means the workload is defined, the critical paths have known capacity envelopes, arrival is controlled, state access distributes, queues are bounded, retries are disciplined, mutating operations are safe to repeat where needed, failure domains are isolated, optional work can degrade, regional capacity is planned, recovery is tested, and the team knows what it expects to hit next.
Find that next ceiling before your players do.
FAQ
Because the thing that breaks first is usually not raw compute. It's a specific dependency that can't absorb the traffic shape: a hot database partition, a throttled login path, a downstream quota, a coupled service. Adding game servers adds capacity at one layer, and does nothing for a ceiling that lives in another. Splitgate's 2021 wall was a 65,536-connection limit in one database, not a shortage of servers.
The front door. Launches arrive as a step, not a ramp, and admitting tens of thousands of new players per second is a different capacity problem from holding a large number concurrently. After that, databases tend to throttle on hot keys, and downstream services amplify load through retries. The order is game-specific, which is why you test for it rather than assume it.
A login queue is admission control, not an admission of failure. It's the backend enforcing the rate at which it can safely accept new work, so a launch spike doesn't hit every downstream service at once. Systems like AccelByte's Login Queue gate on both max concurrency and max login rate per second, and hold the overflow at the door with a visible position and wait time.
It's when one key or partition takes a disproportionate share of traffic, so the database throttles on that object even though the table has spare capacity elsewhere. Player-scoped keys like player:{id} spread well. Shared keys like a global leaderboard, an event record, or a low-cardinality index like status = ONLINE concentrate load. Game events are especially good at turning distributed activity into one hot key overnight.
Enough to cover the interval before autoscaling catches up. A rough model is the rate demand is increasing times the time it takes to provision a server, which is 1 to 10 minutes for a new dedicated server. Size the buffer against short-term demand swings, not the day-night average, and size it per region, because traffic rarely lands where you forecast it.
Test behavior, not endpoints. Virtual players should follow the real loop, log in, party up, matchmake, get a session, play for a realistic duration, write progression, and re-queue, so the test exercises the writes and handoffs that actually break. Then test the worst traffic shapes (instant spike, one-region spike, hot-key event, reconnect storm) and inject failures (latency, 429s, database throttling, a dead region) to see whether the system survives the load while something is broken. Set game-specific acceptance criteria before the run.

Bring your first player online today.
Get started for free, and scale as your game grows.



