Every online game session involves two programs that do not automatically agree with each other: a client (the copy of the game running on a player's own device) and a server (the program holding the one shared, trusted copy of the game state). So far in this curriculum you have mostly written code as if both sides simply see the same truth. In a real multiplayer or online game they don't — a player fully controls the machine their client runs on, and that gap between what a client claims and what actually happened is exactly where cheating lives. This section covers the one rule that everything else builds on, the concrete server-side checks that enforce it, the common categories of cheats explained only at the level needed to defend against them, and the parts of the problem — encryption, account security, kernel-level tools — that sit around the edges of it.
A client and a server constantly exchange small pieces of data called packets (a packet is one unit of data sent over the network, like one envelope in the mail). A packet from a client might say "I pressed the forward key," "I fired my weapon," or "I am now standing at position (120, 4, 88)."
Here is the problem: the player owns the computer the client runs on. They can attach a debugger and pause the game mid-frame. They can open the game's own memory in a tool and change a number while it runs. They can rewrite parts of the game's executable. They can write a program that sits between the game and the network and edits packets before they leave the machine. None of this touches the server at all — it all happens on hardware the player fully controls.
That means every packet a client sends is a claim, not a fact. "I dealt 9999 damage" is not damage that happened by the game's own rules — it is a number a program on the player's own PC decided to send. If the server ever takes a client's claim and applies it to the shared game state without checking it, the game's rules are only as strong as a cheater's willingness to follow them, which is not strong at all.
The clearest way to see why "never trust the client" matters is to look at two versions of the same feature: handling a damage message from an attacking player. Both versions compile. Only one of them is safe to run with real players.
This server-side handler receives a message from the client and directly applies the number the client sent:
// NAIVE SERVER CODE -- do not copy this pattern
class DealDamageMessage
{
public int AttackerId;
public int TargetId;
public int DamageAmount; // taken straight from the client
}
void OnDealDamageReceived(DealDamageMessage msg)
{
Entity target = World.FindEntity(msg.TargetId);
target.Health -= msg.DamageAmount; // trusts the client completely
if (target.Health <= 0)
World.Kill(target);
}
What happens with a modified client: a cheater edits their own client (or writes a program that fakes network packets) so every DealDamageMessage it sends carries DamageAmount = 9999, no matter what weapon they are actually holding.
Nothing about this required touching the server. The cheater only ever touched their own computer.
The fix is not to add an "is this number too big?" check on DamageAmount — a disciplined cheater just picks a number under whatever ceiling you pick. The real fix is to stop trusting the number at all: the server looks up the attacker's real weapon and stats from data it owns, and computes the damage itself.
// VALIDATING SERVER CODE
class DealDamageMessage
{
public int AttackerId;
public int TargetId;
// no DamageAmount field at all -- the client cannot send a
// number the server would need to trust
}
void OnDealDamageReceived(DealDamageMessage msg)
{
Entity attacker = World.FindEntity(msg.AttackerId);
Entity target = World.FindEntity(msg.TargetId);
if (attacker == null || target == null) return;
if (!attacker.IsAlive || !target.IsAlive) return;
if (!InAttackRange(attacker, target)) return; // sanity check
if (!OffCooldown(attacker, "attack")) return; // rate limit
// server computes the real number itself, from data
// only the server is allowed to change
int damage = CombatRules.ComputeDamage(attacker.Weapon,
attacker.Level,
attacker.Buffs,
target.Armor);
target.Health -= damage;
StartCooldown(attacker, "attack");
if (target.Health <= 0)
World.Kill(target);
}
Trace with the same modified client: the cheater's client can still send a DealDamageMessage as fast as it wants, but DamageAmount no longer exists as a field the server reads. The server calculates 8 damage for a starting dagger every single time, no matter what the client hoped it would say. Editing the client changed nothing about the outcome.
This is the core shift: a validating server does not just check that client data looks reasonable — it treats client input as an intent ("I am attacking this target") and recomputes the result itself from server-owned data. That one change closes off an entire category of cheats at once.
if (msg.DamageAmount > 500) reject(); and calling the problem solved. This stops the most obvious abuse (9999 damage) but not a disciplined cheat that sends 400 damage every hit — still far above what any real weapon does, but under the ceiling. A bounds check is a useful second layer, never the only layer, for a value the client should not have been trusted to send in the first place.The pattern from Section 2 has a name: server authority (the rule that the server holds the one true copy of the game state, and clients only hold a copy they are not allowed to force changes onto). Almost every specific technique in this section is the same idea applied to a different kind of data: position, health, inventory, currency, cooldowns.
Contrast two architectures:
Under a client-authoritative design, a modified client can claim to be anywhere, have any amount of health, or own any item, and the server has no independent way to know it is wrong. Under a server-authoritative design, the client only ever sends inputs (button presses, aim direction, an item it wants to use) and the server is the only program allowed to turn those inputs into a new game state. The client then just displays whatever state the server sends back.
Server authority is not free. It costs the server CPU time — it has to simulate the whole game, not just referee it — and it adds a small delay before a client sees the confirmed result of its own action, which is why games often use client-side prediction (drawing the likely result immediately, then quietly correcting it if the server later disagrees) to hide that delay. That cost is exactly why server authority is the primary defense rather than a free one: studios pay real engineering effort for it, because the alternative is a game that cannot be trusted at all.
Position is one of the most commonly cheated values, because "being somewhere you should not be able to reach yet" is directly useful — finishing a race early, standing inside a wall to avoid being hit, or reaching a rare resource before anyone else can. A server-authoritative movement system still needs a concrete check, because a fake input (a claimed position, a claimed huge time delta) can still slip through unless the server double-checks the result against reality.
The check is simple: given a player's last known position and how much real time has passed, there is a maximum distance they could physically have covered. Any claimed position beyond that maximum is impossible under the game's own rules.
class PlayerMoveState
{
public Vector3 LastPosition;
public float LastServerTime;
public float MaxSpeed; // units per second, from this player's stats
}
bool TryMove(PlayerMoveState state, Vector3 claimedPosition, float serverNow)
{
float dt = serverNow - state.LastServerTime;
if (dt <= 0f) return false; // out-of-order or replayed packet
float distance = Vector3.Distance(state.LastPosition, claimedPosition);
float maxDistance = state.MaxSpeed * dt * 1.15f; // +15% tolerance for lag
if (distance > maxDistance)
{
Flag(state, "movement speed exceeded"); // impossible -- reject
return false; // client is snapped back to LastPosition
}
state.LastPosition = claimedPosition;
state.LastServerTime = serverNow;
return true;
}
Trace, honest player: max speed 6 units/second, 0.1 seconds since the last update -> max distance = 6 * 0.1 * 1.15 = 0.69 units. A normal step of 0.5 units passes easily.
Trace, teleport hack: the same player's claimed position jumps 500 units inside that same 0.1-second window.
The tolerance factor (the 1.15f above) exists because network delay means the server never has a perfectly up-to-date picture — a small amount of slack is needed so honest players with normal lag are not constantly rejected. Too tight causes false rejections for laggy but honest players; too loose stops catching anything but the most extreme teleports.
Speed and position are not the only things a modified client can exaggerate. Any action with a cooldown or a limited frequency — firing a weapon, casting a spell, opening a shop, claiming a daily reward — can be abused by a client that sends the "do this" message far faster than a legitimate client (and a human pressing a real button) ever could. This is a cruder cousin of a speed hack: instead of moving faster, the cheat acts faster.
The defense is a per-action cooldown tracked entirely on the server, checked before every action is allowed to happen, independent of anything the client says about its own timing:
class ActionCooldowns
{
Dictionary<string, float> lastUsedServerTime = new Dictionary<string, float>();
public bool TryUse(string actionName, float cooldownSeconds, float serverNow)
{
if (lastUsedServerTime.TryGetValue(actionName, out float last))
{
if (serverNow - last < cooldownSeconds)
return false; // still on cooldown -- refuse the action
}
lastUsedServerTime[actionName] = serverNow;
return true;
}
}
Trace: a fireball spell has a 2-second cooldown. A modified client fires 40 CastSpell("fireball") messages in one second. TryUse lets the first one through and refuses the other 39, because less than 2 seconds have passed since the last accepted use — regardless of how many messages arrived. The player's screen effects might spam, but only one fireball ever actually exists in the shared game state.
Sections 2, 4, and 5 each showed one specific check — computing damage instead of trusting it, bounding movement distance, limiting action rate. In a real game these checks follow the same shape every time. Here is that shape as pseudocode, general enough to apply to almost any action a client requests:
function HandleClientAction(playerId, action, serverNow):
player = LookupPlayer(playerId)
// 1. Identity and ownership -- is this really this player's
// connection, and do they own what they claim to act on?
if player == null or not player.IsConnected:
return Reject("unknown or disconnected player")
if action.RequiresOwnership and not player.Owns(action.TargetItemId):
return Reject("does not own target item")
// 2. State preconditions -- is this action even legal right now?
if not player.IsAlive:
return Reject("dead players cannot act")
if not player.IsInRange(action.TargetId, action.MaxRange):
return Reject("target out of range")
// 3. Rate limiting -- has enough real server time passed?
if not player.Cooldowns.TryUse(action.Name, action.CooldownSeconds, serverNow):
return Reject("action on cooldown")
// 4. Compute the result from server-owned data -- never from
// any numeric value the client itself supplied
result = ComputeResult(player, action) // e.g. damage, loot, currency
// 5. Apply to the one shared game state, then tell every
// relevant client what actually happened
ApplyToWorld(result)
BroadcastToRelevantClients(result)
return Accept(result)
Every numbered step closes off one category of cheat: step 1 stops a cheater acting as someone else or on items they do not own, step 2 stops actions that break the game's own rules of sequence (attacking while dead, hitting something across the map), step 3 stops speed and rate abuse, and step 4 is the Section 2 lesson generalized — the server is the only thing allowed to decide what a "result" actually is.
It helps to name the actual cheat categories the previous sections defend against, at the level needed to understand the defense — not as a how-to guide. The first category is memory manipulation: a tool that looks inside the game process's own memory (the numbers the running program is currently using, such as current ammo, current health, or current currency) and changes a value directly while the game runs, entirely on the cheater's own machine. A related technique attaches a debugger to the running client and edits values or skips code the same way.
This sounds powerful, and on a client-authoritative game it is: if the client's copy of "ammo = 30" is also the number the server trusts, editing it to 999 directly wins. But look at what it actually changes: the bytes inside the client process. Nothing about editing your own computer's memory reaches across the network and changes the server's separate copy of that same value.
Defense: this is exactly server authority from Section 3. If the server is the only thing that decrements ammo when a shot is fired, and the server's next update simply overwrites whatever the client is showing, editing the client's memory changes what the cheater's own screen displays for at most a moment, and changes nothing about the shared game state everyone else sees. Any value that matters to fairness — health, currency, inventory, cooldowns, position — needs to live on the server, not just be mirrored there.
A speed hack tries to make the client believe more time has passed than actually has — speeding up the client's own clock, or directly claiming a larger deltaTime (the amount of time since the last update, normally measured by the game engine itself) — so anything computed from time (movement, cooldown timers, animation) runs faster than it should.
The defense already exists in this section: Sections 4 and 5 both compute time from the server's own clock, never from a value the client reports about itself.
// VULNERABLE: trusts a client-reported time delta
void OnMoveRequest(MoveMessage msg)
{
// msg.ClientDeltaTime is whatever the client's cheat tool wants it to be
player.Position += player.InputDirection * player.Speed * msg.ClientDeltaTime;
}
// SAFE: server measures elapsed time itself
void OnMoveRequest(MoveMessage msg)
{
float dt = ServerClock.Now - player.LastUpdateServerTime; // server's own clock
player.Position += player.InputDirection * player.Speed * dt;
player.LastUpdateServerTime = ServerClock.Now;
}
The vulnerable version lets a modified client simply report ClientDeltaTime = 1.0 on every single network tick, moving the player a full second's distance dozens of times a second. The safe version cannot be fooled this way, because the number it multiplies by comes from the server's own clock, which the cheater's machine has no way to reach into. This is the same principle as Section 4's distance cap, applied to the time input instead of the position output — a real game normally uses both checks together.
A wallhack renders enemies (or items, or objectives) through walls and other geometry that should be hiding them. An aimbot automatically snaps a player's aim onto an enemy, often using the same underlying information a wallhack uses. Neither cheat forges a number the way a damage hack does — both read information that is already sitting on the cheater's own machine and either display it or act on it.
That is the key detail: in a carelessly built game, the server sends every client the position of every entity in the whole level, all the time, and simply relies on the client's own renderer to decide what to draw — hidden things are "hidden" only because the normal game code chooses not to draw them. A modified client can just draw them anyway, or feed those positions straight into an aim-assist routine. The data was never actually secret; it was sitting in the client's memory the whole time, delivered there by the server itself.
Defense: interest management (also called network-level visibility culling) means the server computes, separately for each client, which entities that specific client is currently allowed to know about — based on line-of-sight (whether a straight line to the entity is blocked by geometry), distance, or deliberate design like fog of war — and only includes those entities in the data it sends to that client. If an enemy's position is never sent to a client at all, there is nothing in that client's memory for a wallhack to read or draw, no matter how the memory is scanned.
List<Entity> BuildVisibleSnapshot(Player viewer, List<Entity> allEntities)
{
List<Entity> visible = new List<Entity>();
foreach (Entity e in allEntities)
{
if (e == viewer.Entity) { visible.Add(e); continue; }
float dist = Vector3.Distance(viewer.Position, e.Position);
if (dist > viewer.MaxViewDistance) continue; // too far
if (!HasLineOfSight(viewer.Position, e.Position)) continue; // blocked
visible.Add(e); // only entities that pass both checks are sent
}
return visible;
}
Trace: player A stands in an open field with an enemy 20 units away (line of sight clear, inside MaxViewDistance) and another enemy 5 units away but on the other side of a solid wall. BuildVisibleSnapshot includes the first enemy and excludes the second — the network packet sent to player A's client literally does not contain the second enemy's position. A wallhack running on player A's machine has no data to expose, because the server-side filter already ran before anything was sent.
Aimbots are only partly solved by interest management. It removes the data for enemies a player should not see at all, but a legitimately visible enemy's position has to be sent for the honest game to render it, and an aimbot can lock onto that. Catching an aimbot aimed at a legitimately visible target needs a different kind of defense, covered next.
Not every cheat can be closed off by a validation rule. An aimbot aimed at an enemy the player is legitimately allowed to see is not doing anything the server's rules can reject outright — "aim precisely at a visible target" is also exactly what a very skilled human player does. The same is true for cheats that read visible information faster than a human could react to, without touching any value the server owns.
The defense here is different in kind: instead of checking one action against a hard rule, the server (or a separate analytics service) records statistics about a player's behavior over time — accuracy percentage, headshot rate, how consistently their aim tracks a moving target through weapon recoil, reaction time from an enemy appearing to the player first shooting — and flags players whose numbers sit far outside the range real human players produce.
class AccuracyTracker
{
int shotsTaken = 0;
int shotsHit = 0;
public void RecordShot(bool hit)
{
shotsTaken++;
if (hit) shotsHit++;
if (shotsTaken >= 50) // only judge once there is enough data
{
float accuracy = (float)shotsHit / shotsTaken;
// 92%+ sustained accuracy over 50+ shots is far above
// typical human performance for this weapon type
if (accuracy > 0.92f)
FlagForReview(accuracy, shotsTaken);
}
}
}
Trace: a legitimate skilled player might sustain 35-55% accuracy with a fast-firing weapon across a real match. A player whose tracked accuracy sits at 96% over 200 shots is not impossible by any single rule the server can check in the moment, but it is far enough outside the normal human range to be worth a closer look — either automated (tighter monitoring kicks in) or a human reviewer watching a recording of that match.
Every defense so far assumes the packets the server receives are the ones the client actually sent, unmodified in transit, and that packets the server sends are received only by the intended client. That assumption needs its own defense: the network path between client and server is a channel a third party can potentially read or tamper with — from the same Wi-Fi network, a compromised router, or a proxy tool the cheater runs on their own machine on purpose.
Two separate protections matter here:
https:// web traffic, or a game-specific encrypted protocol) means a generic packet-sniffing or packet-editing tool sees scrambled bytes instead of a readable message like DamageAmount=8. This does not stop a cheat built into the client itself (it already has the unencrypted data before encryption happens), but it stops a much easier class of attack: a separate tool intercepting and rewriting packets in transit without ever touching the game's own code.Everything so far defends the game itself from being manipulated mid-match. A separate attack surface is the player's account (the identity and progress record tied to a login) — an attacker who is not trying to cheat inside a match at all, but trying to break into someone else's account to steal purchased items, resell it, or use it to spread more cheating.
Three practical defenses cover most of this surface:
class LoginRateLimiter
{
Dictionary<string, List<float>> recentAttempts = new Dictionary<string, List<float>>();
const int MaxAttemptsPerWindow = 5;
const float WindowSeconds = 60f;
public bool AllowAttempt(string account, float nowServerTime)
{
if (!recentAttempts.ContainsKey(account))
recentAttempts[account] = new List<float>();
List<float> attempts = recentAttempts[account];
attempts.RemoveAll(t => nowServerTime - t > WindowSeconds); // drop old ones
if (attempts.Count >= MaxAttemptsPerWindow)
return false; // too many recent attempts -- block for now
attempts.Add(nowServerTime);
return true;
}
}
Trace: a script tries 200 passwords against one account in ten seconds. The first 5 attempts within the 60-second window are allowed through to the real password check (and fail, since the guesses are wrong); attempt 6 onward is blocked by AllowAttempt before it even reaches the password check, regardless of how many more the script sends. A genuine player who mistypes their password twice is barely affected.
Every defense in this section can be, and eventually is, worked around by someone determined enough — there is a real market of people who build and sell cheat tools, and they update those tools when a studio patches a hole. Anti-cheat is not a problem a studio solves once; it is ongoing work, closer to how a bank keeps defending against fraud than to fixing a single bug.
Three realities of that ongoing fight, stated plainly:
The honest summary: none of these techniques, alone or combined, make a game permanently cheat-proof. The realistic goal is to make cheating expensive and risky enough, and detection fast enough, that the large majority of players get a fair match — not to reach zero cheating, which no shipped multiplayer game has ever actually achieved.
class BuyItemMessage
{
public int ItemId;
public int Price; // sent by the client
public int PlayerGold; // sent by the client, "for convenience"
}
void OnBuyItem(BuyItemMessage msg, Player player)
{
if (msg.PlayerGold >= msg.Price)
{
player.Inventory.Add(msg.ItemId);
player.Gold = msg.PlayerGold - msg.Price;
}
}
The handler trusts two values it should never trust: msg.Price (the client could claim any item costs 0 gold) and msg.PlayerGold (the client could claim to have any amount of gold at all, and the server then overwrites its own record of the player's gold with whatever the client said). This is worse than Section 2's naive damage handler, because it does not just misapply one value — it lets the client dictate the player's entire gold balance going forward.
The fix follows the same pattern as Section 2: look up real data on the server and never take a number the client suggests about itself.
class BuyItemMessage
{
public int ItemId; // only the intent -- which item -- is needed
}
void OnBuyItem(BuyItemMessage msg, Player player)
{
ItemDef item = ItemDatabase.Lookup(msg.ItemId); // server-owned price
if (item == null) return;
if (player.Gold < item.Price) return; // server's own gold record
player.Inventory.Add(msg.ItemId);
player.Gold -= item.Price; // computed from server state, not received
}
Now the client can only say which item it wants; the price and the player's current gold both come from data the server owns, so there is nothing left for a modified client to lie about.
bool TryUsePotion(PlayerState player, float serverNow) that only allows a potion to be used once every 3 real seconds, using the server's own clock. Then trace what happens if a modified client sends 10 UsePotion requests within the same 0.2-second window.class PlayerState
{
public float LastPotionServerTime = -999f; // far in the past initially
}
bool TryUsePotion(PlayerState player, float serverNow)
{
const float cooldownSeconds = 3f;
if (serverNow - player.LastPotionServerTime < cooldownSeconds)
return false; // still on cooldown, reject
player.LastPotionServerTime = serverNow; // record using server's clock
// ... apply the potion's real effect here ...
return true;
}
Trace: 10 requests arrive between serverNow = 10.0 and serverNow = 10.2. The first request (at 10.0) passes, because 10.0 - (-999) >= 3, and sets LastPotionServerTime = 10.0. Every one of the other 9 requests arrives before serverNow reaches 13.0, so each one fails the check and is rejected. Exactly one potion is used, no matter how many requests the modified client sends, because the check is based entirely on the server's own clock rather than anything the client claims about timing.
BuildVisibleSnapshot pattern from Section 9, sketch how you would extend the visibility check to also hide crouched-in-grass players beyond 8 units, and explain in one or two sentences why this stops a wallhack (or a similar cheat) from ever revealing a hidden player, no matter what the cheat does on the client.bool CanSee(Player viewer, Entity target)
{
float dist = Vector3.Distance(viewer.Position, target.Position);
if (target.IsCrouchedInGrass && dist > 8f)
return false; // hidden by the stealth rule, regardless of line of sight
if (dist > viewer.MaxViewDistance) return false;
if (!HasLineOfSight(viewer.Position, target.Position)) return false;
return true;
}
List<Entity> BuildVisibleSnapshot(Player viewer, List<Entity> allEntities)
{
List<Entity> visible = new List<Entity>();
foreach (Entity e in allEntities)
if (e == viewer.Entity || CanSee(viewer, e))
visible.Add(e);
return visible;
}
This works for the same reason interest management defeats an ordinary wallhack: the check runs entirely on the server, before the network packet describing what the viewer can see is ever built. A crouched player beyond 8 units simply is not one of the entities included in that packet, so their position never reaches the enemy's client at all. No amount of memory editing, packet reading, or client modification on the enemy's machine can expose data that was never sent to that machine in the first place — the secret was kept on the server, not hidden-but-present on the client.