Lesson 6: Game Server - Performance Optimization
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Why Optimize Now
From working server to durable server
In Lesson 4: Game Server - Basics, we built the authoritative server loop: sessions, players, message dispatch, and world ownership.
In Lesson 5: Game Server - Persistence, we added a save boundary so the server can survive restarts without turning disk I/O into gameplay logic.
Now we ask the scary MMORPG question:
What happens when 10 players becomes 100, then 1,000?
Performance optimization is not about sprinkling inline everywhere. It is about protecting the server's most important promise: each tick completes on time.
In this lesson, we'll focus on server-side techniques that matter before graphics, editor tools, or cross-platform packaging enter the picture:
- Measuring before changing code
- Preserving a tick budget
- Avoiding unnecessary world scans
- Reducing allocation pressure
- Handling overload gracefully
You can think of this as the C++ equivalent of what you'd do in a Netty service: observe latency, identify bottlenecks, reduce contention, and apply backpressure before the system falls over.

Measure Before Optimizing
Optimization starts with evidence
A common trap is optimizing the code that looks expensive instead of the code that actually burns the frame.
For our server, we care about questions like:
- How long does each tick take?
- Which phase is slow: input, simulation, visibility, persistence handoff, networking?
- Is the slowdown occasional or constant?
- Are we CPU-bound, allocation-bound, lock-bound, or I/O-bound?
For an early MMORPG server, a tiny scoped timer is enough to create useful logs. Later, you can graduate to platform profilers, flame graphs, ETW on Windows, Tracy, Perfetto, or custom telemetry.
The key habit is the same: make time visible.
```cpp
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
class ScopedTimer {
public:
explicit ScopedTimer(std::string name)
: name_(std::move(name)), start_(std::chrono::steady_clock::now()) {}
~ScopedTimer() {
auto end = std::chrono::steady_clock::now();
auto micros = std::chrono::duration_cast<std::chrono::microseconds>(end - start_).count();
std::cout << name_ << " took " << micros << " us\n";
}
private:
std::string name_;
std::chrono::steady_clock::time_point start_;
};
void receiveMessages() {
ScopedTimer timer("receiveMessages");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
void simulateWorld() {
ScopedTimer timer("simulateWorld");
std::this_thread::sleep_for(std::chrono::milliseconds(3));
}
void buildSnapshots() {
ScopedTimer timer("buildSnapshots");
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
int main() {
{
ScopedTimer tick("server tick");
receiveMessages();
simulateWorld();
buildSnapshots();
}
std::cout << "Use these timings to choose the next optimization target.\n";
return 0;
}
```
Find The Bottleneck
Your server tick has a 50 ms budget. Logs show this average breakdown:
- Receive client messages: 2 ms
- Decode packets: 1 ms
- Simulate world: 38 ms
- Queue persistence saves: 1 ms
- Build outgoing snapshots: 4 ms
Which optimization target should you investigate first?
Protect Tick Budget
The tick is your contract
In Lesson 4, the server became authoritative. That means the server tick is where truth advances.
A common server rate is 20 ticks per second:
1 second / 20 ticks = 50 ms per tick
That does not mean every subsystem gets to spend 50 ms. The whole tick must fit:
50 ms tick budget
├─ receive and drain messages
├─ apply validated input
├─ simulate world
├─ resolve combat and AI
├─ update visibility
├─ enqueue persistence work
└─ prepare outgoing snapshots
If a tick takes 80 ms, the next tick starts late. If that repeats, the server enters a spiral:
slow tick → backlog grows → more work next tick → slower tick
Performance optimization is really budget enforcement.

Avoid World Scans
The hidden killer: checking everything
A naive server often asks questions like:
- Which monsters are near this player?
- Which clients should receive this movement update?
- Which entities are inside this area effect?
The slow version scans every entity:
for every player:
for every entity in world:
check distance
That may work for a prototype, but MMORPG worlds are large. A practical first step is a spatial partition: divide the world into grid cells and only query nearby cells.
This is not yet a map editor feature. It is a runtime server structure that helps the authoritative world answer local queries cheaply.
```cpp
#include <cmath>
#include <iostream>
#include <unordered_map>
#include <vector>
struct Entity {
int id;
float x;
float y;
};
struct CellKey {
int x;
int y;
bool operator==(const CellKey& other) const {
return x == other.x && y == other.y;
}
};
struct CellKeyHash {
std::size_t operator()(const CellKey& key) const {
return std::hash<int>()(key.x * 73856093 ^ key.y * 19349663);
}
};
class SpatialGrid {
public:
explicit SpatialGrid(float cellSize) : cellSize_(cellSize) {}
void insert(const Entity& entity) {
cells_[cellFor(entity.x, entity.y)].push_back(entity);
}
std::vector<Entity> queryNearby(float x, float y, float radius) const {
std::vector<Entity> result;
int minCellX = static_cast<int>(std::floor((x - radius) / cellSize_));
int maxCellX = static_cast<int>(std::floor((x + radius) / cellSize_));
int minCellY = static_cast<int>(std::floor((y - radius) / cellSize_));
int maxCellY = static_cast<int>(std::floor((y + radius) / cellSize_));
for (int cy = minCellY; cy <= maxCellY; ++cy) {
for (int cx = minCellX; cx <= maxCellX; ++cx) {
auto it = cells_.find(CellKey{cx, cy});
if (it == cells_.end()) continue;
for (const Entity& entity : it->second) {
float dx = entity.x - x;
float dy = entity.y - y;
if (dx * dx + dy * dy <= radius * radius) {
result.push_back(entity);
}
}
}
}
return result;
}
private:
CellKey cellFor(float x, float y) const {
return CellKey{
static_cast<int>(std::floor(x / cellSize_)),
static_cast<int>(std::floor(y / cellSize_))
};
}
float cellSize_;
std::unordered_map<CellKey, std::vector<Entity>, CellKeyHash> cells_;
};
int main() {
SpatialGrid grid(10.0f);
for (int i = 0; i < 1000; ++i) {
grid.insert(Entity{i, static_cast<float>(i % 100), static_cast<float>(i / 10)});
}
std::vector<Entity> nearby = grid.queryNearby(50.0f, 50.0f, 8.0f);
std::cout << "Naive scan would inspect 1000 entities.\n";
std::cout << "Spatial query returned " << nearby.size() << " nearby entities.\n";
std::cout << "First few ids:";
for (std::size_t i = 0; i < nearby.size() && i < 8; ++i) {
std::cout << ' ' << nearby[i].id;
}
std::cout << "\n";
return 0;
}
```
Scanning Is Fine
True or false: If the server has a small prototype world today, it is always safe to keep every visibility, combat, and area query as a full scan over all entities.
Allocation Pressure
Fast code can still stall
In C++, memory allocation is often a server performance problem long before arithmetic is.
Watch for patterns like:
- Creating temporary
std::vectors inside hot loops - Building strings for logs every tick
- Allocating one packet object per tiny message
- Rehashing maps during gameplay spikes
- Copying large world snapshots unnecessarily
This does not mean "never use the standard library." It means use it deliberately:
- Reserve capacity for known workloads
- Reuse buffers where ownership is clear
- Prefer stable IDs over pointer-heavy object graphs
- Keep hot tick data compact and predictable
- Move logging and persistence work away from hot paths when possible
The goal is not C-style minimalism. The goal is to avoid surprising the tick with hidden allocation work.

Name The Technique
When many entities exist, the optimization that groups entities by location so nearby queries inspect only relevant buckets instead of the whole world is called ______.
Handle Overload Gracefully
Do bounded work per tick
A production-minded server should assume overload will happen:
- Too many movement messages arrive at once
- A database or filesystem stalls
- A player enters a crowded area
- A bug causes a burst of repeated requests
The answer is not always "process everything immediately." If the tick has a budget, some work must be delayed, merged, rejected, or degraded.
For example, movement input is often coalescible. If one client sends several movement intents before the next simulation step, the latest intent may be the only one that matters.
This is the same family of thinking as backpressure in backend systems: preserve the health of the server instead of letting queues grow without limit.
```cpp
#include <deque>
#include <iostream>
#include <string>
#include <unordered_map>
struct MoveIntent {
int playerId;
int sequence;
std::string direction;
};
int main() {
std::deque<MoveIntent> incoming = {
{1, 10, "north"}, {1, 11, "north"}, {1, 12, "east"},
{2, 20, "south"}, {3, 30, "west"}, {2, 21, "east"},
{4, 40, "north"}, {5, 50, "south"}
};
const std::size_t maxMessagesToDrain = 5;
std::unordered_map<int, MoveIntent> latestMoveByPlayer;
std::size_t drained = 0;
while (!incoming.empty() && drained < maxMessagesToDrain) {
MoveIntent intent = incoming.front();
incoming.pop_front();
++drained;
// Coalesce: keep only the latest movement intent per player for this tick.
latestMoveByPlayer[intent.playerId] = intent;
}
std::cout << "Drained messages this tick: " << drained << "\n";
std::cout << "Deferred messages: " << incoming.size() << "\n";
std::cout << "Movement intents applied this tick:\n";
for (const auto& pair : latestMoveByPlayer) {
const MoveIntent& intent = pair.second;
std::cout << " player " << intent.playerId
<< " seq " << intent.sequence
<< " dir " << intent.direction << "\n";
}
return 0;
}
```
Optimization Checklist
What we improved conceptually
At this point, our server is no longer just "working." It has a performance mindset:
- Measure first with timing around tick phases
- Protect the tick budget instead of chasing vague speed
- Avoid full-world scans for local queries
- Reduce allocation pressure in hot paths
- Bound work per tick so overload does not become collapse
- Coalesce or defer work when exact immediate processing is unnecessary
A useful rule for MMORPG servers:
Optimize for predictable ticks before optimizing for maximum theoretical throughput.
This completes the core server arc of the course: authority, persistence, and performance. Next, we'll shift back to the client side and start building god-mode editing tools, beginning with the map editor.
