Detecting cheats in a browser game: server-side checks that actually work

Every browser game ships its entire client to the player: the code, the constants, the WebSocket messages, and a DevTools console that can rewrite any of it. You cannot hide logic in a browser, so client-side anticheat is at best a speed bump. What actually works is boring: the server decides what happened, and it checks every claim the client makes against physics, time and rules it can compute itself. This post is the checklist I use — the sanity checks that catch real cheats, the ones that only look clever, and how to act on a flag without banning honest players on bad Wi-Fi.

Rule zero: send intent, not outcomes

If a client can send { type: 'score', value: 99999 } or { type: 'hit', target: 7, damage: 100 }, you have already lost, and no amount of obfuscation will fix it. The client should send inputs — "move left held on tick 4812", "fire pressed, aim angle 1.37" — and the server should run the simulation that turns inputs into positions, hits and scores. This is the same server-authoritative loop I described in the tick-rate post; anticheat is mostly a side effect of doing that loop properly.

Once the server owns outcomes, the cheats that remain are the ones that abuse inputs: sending them too fast, sending impossible ones, sending them with a forged clock, or reading information the client should not have. Each has a concrete server-side check.

every input runs the gauntlet · failures add suspicion, not bans client inputs only 1 schema + rate limit 2 clock tick window 3 physics + rules simulation owns outcomes suspicion score (decays over time) clamp + log → review → shadow queue → ban leaderboards: store seed + input log, re-simulate server-side, accept only the recomputed score

Check 1: schema and rate

Validate every message shape before it touches game state — types, ranges, enum values, array lengths. An aim angle of NaN or a movement vector of length 40 is not a "cheat" so much as a crash waiting to happen, and NaN in particular poisons every calculation it touches. Normalise direction vectors on the server; never trust their magnitude.

Then rate-limit per connection. A client at 60 Hz sends roughly 60 inputs a second; allow a burst for jitter and reject the rest with a token bucket:

// refill 60 tokens/s, burst of 90 absorbs network clumping
function allow(c, now) {
  c.tokens = Math.min(90, c.tokens + (now - c.last) * 0.06);
  c.last = now;
  if (c.tokens < 1) return false;
  c.tokens -= 1; return true;
}

Rate limiting alone defeats auto-clickers and "fire every frame" scripts on anything with a cooldown — as long as the cooldown lives on the server too.

Check 2: the clock is the server's

Speed hacks in browser games usually work by accelerating the client's notion of time — overriding performance.now or requestAnimationFrame so the game loop runs 1.5× fast. If your server integrates movement from its own tick, that simply does nothing: extra inputs get rate-limited or collapse onto the same tick. If your server accepts client timestamps, it is exploitable.

Inputs should be stamped with the tick they apply to, and the server should accept only a window around its current tick: nothing from the future, and nothing older than your lag-compensation limit (a common choice is a couple of hundred milliseconds). Also track the ratio of input ticks to wall-clock time over a longer window, say 10 seconds. An honest client averages 1.0 with noise; a client consistently at 1.1 or above is running a fast clock, and that pattern is very hard to hide.

Check 3: physics and rules

Even if the client only sends inputs, many games let it report its own position for smooth prediction. Then the server must check the claim, not accept it:

const dt = (tick - p.lastTick) / TICK_HZ;
const maxDist = p.maxSpeed * dt * 1.15 + 0.5; // 15% + small slack for rounding
if (Math.hypot(x - p.x, y - p.y) > maxDist) {
  flag(p, 'speed', 2);
  correct(p); // snap back to the server's position
}

The tolerance matters: jitter and packet bunching make honest players look briefly fast, so the correct response to a single violation is to correct, not punish. The same pattern covers the rest of the rulebook:

  • Teleport / noclip: raycast the claimed path against collision geometry, not just the distance.
  • Cooldowns, ammo, currency: server-owned counters only. The client may display them; it never decrements them.
  • Hits: rewind targets to the shooter's view time (clamped to your lag-comp limit) and re-test the ray. I cover the geometry in the hitscan vs projectile post.
  • Aim: statistical, not per-shot — headshot ratio, reaction time from a target becoming visible to fire, and angular snap speed, compared to the population.

Check 4: outcomes you can recompute (leaderboards)

Single-player games with a global leaderboard are the most cheated category, because there is no live server to own the simulation. The fix is replay verification: the server issues a seed at game start, the client records its input log, and on submission the server re-runs the game headlessly and keeps only the score it computes. That needs a deterministic simulation — a seeded PRNG, a fixed timestep, and care with floats (see fixed-point maths) — and the input-log replay format doubles as the submission format.

Two cheap extras: reject runs whose real elapsed time (server-side, from seed issue to submission) is shorter than the simulated duration, and make seeds single-use so a "perfect run" cannot be replayed on another account.

What does not work (on its own)

  • Signing payloads with a key in the client. Anything the browser can sign, a cheater can sign. A server-issued HMAC session token proves the session is real, not that the score is.
  • Obfuscation and anti-DevTools tricks. They raise the effort bar for casual tampering and cost you debuggability. Treat them as noise reduction, never as a security boundary.
  • Client-side detection. Checking for modified globals or unusual frame timing can generate useful telemetry, but the result is still reported by the client, so it can be lied about.

Information cheats: do not send what they should not see

Wallhacks and map reveals need no input manipulation at all — they read state you already sent. The only defence is interest management: do not send enemy positions a player cannot see or hear, and do not send the hidden card, the mine layout or the next crash point. The provably fair mines design is the extreme version of this: the outcome is committed as a hash and only revealed after the round.

Acting on flags without banning honest players

Give each connection a suspicion score. Each failed check adds a weight (a single speed correction: 1; an impossible teleport through a wall: 10; a clock ratio of 1.3 sustained for a minute: 25) and the score decays over time. Then escalate in steps: clamp and correct, log with the input window attached, queue for review, move to a shadow matchmaking pool, and only then ban. Delaying bans also stops cheaters from learning exactly which check tripped.

Checklist

  1. Clients send inputs; the server computes outcomes.
  2. Validate schema and ranges; reject NaN; normalise vectors.
  3. Token-bucket rate limit per connection; server-owned cooldowns.
  4. Accept inputs only inside a tick window; watch the tick-to-wall-clock ratio.
  5. Speed and path checks with tolerance; correct first, flag second.
  6. Rewind-and-retest hit validation with a capped lag window.
  7. Leaderboards: seed + input log + server re-simulation, single-use seeds.
  8. Interest management: never send hidden state.
  9. Decaying suspicion score, staged escalation, delayed bans.
→ Server-authoritative multiplayer on a budget → Replay systems from an input log → Lockstep vs rollback netcode → Portfolio & projects
Can you stop cheating in a browser game?

You cannot stop players modifying the client, because the browser ships them all of your code. You can make cheating useless by keeping the server authoritative: clients send inputs, the server simulates outcomes, and every claim is checked against rate limits, a server-owned clock, physics and game rules.

How do you detect a speed hack in a JavaScript game?

Never integrate movement from client timestamps. Stamp inputs with a tick, accept only a window around the server's current tick, compare claimed movement against max speed times elapsed server time with a small tolerance, and track the ratio of input ticks to wall-clock time over about 10 seconds. A sustained ratio above roughly 1.1 indicates an accelerated client clock.

How do I stop fake scores on a browser game leaderboard?

Issue a single-use seed from the server when the run starts, have the client record its input log, and on submission re-simulate the game server-side with that seed and log. Store only the score the server computes, and reject runs whose real elapsed time is shorter than the simulated time. This requires a deterministic simulation.

Does obfuscating my JavaScript prevent cheating?

No. Obfuscation and anti-DevTools tricks raise the effort needed for casual tampering but are not a security boundary. Likewise, signing requests with a key embedded in the client proves nothing, because the cheater has the key too.

Should I ban a player the first time a check fails?

No. Network jitter makes honest players trip movement checks occasionally. Correct the state first, add a weighted amount to a decaying suspicion score, and escalate in stages: log, review, shadow pool, then ban. Delayed bans also hide which check detected the cheat.