Quick Answer

Real-time multiplayer games hide network latency by having the client simulate the local player's movement immediately (client-side prediction) instead of waiting for a server round trip, then correcting the client's position if the server's authoritative simulation disagrees (reconciliation). We built and ran a real 1D Python simulation showing the exact predicted, corrected, and final position values with a simulated 150ms one-way network delay.

The Problem: You Can't Wait for the Network

The naive way to build a multiplayer game is: the client sends your input to the server, the server simulates the result, and the server sends back the new state for the client to render. This is simple and it's also basically unplayable for anything that needs precise timing. Even a generous 50ms one-way connection means a 100ms round trip before you see the result of your own keypress — real internet connections regularly run higher than that, and cross-region or mobile connections higher still. A tenth of a second of input delay, minimum, is fine for a turn-based game and immediately noticeable in a shooter, a fighting game, or a platformer.

At the same time, you can't just let the client decide its own outcome and tell the server about it afterward — that's an open invitation to cheat, since nothing stops a modified client from just lying about its position or the damage it dealt. So the real constraint is two requirements that pull in opposite directions: the server has to stay authoritative, meaning nothing is real until the server agrees it happened, while the client has to feel instant, meaning the player can't be made to wait for that agreement before seeing a result. Client-side prediction and server reconciliation are the answer the industry converged on to satisfy both at once.

Client-Side Prediction: Acting First, Asking Permission Second

The fix is for the client to stop waiting. The instant a player presses a key, the client applies that input to its own local copy of the simulation and renders the result immediately — while, in parallel, sending that same input to the server over the network. The player never sees a delay for their own actions, because nothing is actually waiting on the network at all; the network call is happening alongside the rendering, not before it.

For this to be correctable later, the client has to remember what it did. Every input gets tagged with an increasing sequence number and stored in a local history buffer, along with the position that input produced, before it's sent off. The client doesn't get to forget an input just because it applied it already — it has to hold onto it until the server explicitly confirms it, because the server might not agree, and when it doesn't, the client is going to need to redo some work.

Server Reconciliation: Correcting Prediction When It's Wrong

Inputs arrive at the server delayed by the network. The server runs the same simulation logic against its own authoritative world state and sends back the resulting authoritative position, tagged with the sequence number of the last input it actually processed. When that update reaches the client — delayed again, by the return trip — the client looks up its own remembered predicted position for that exact sequence number and compares the two.

If they match, nothing visible happens: the prediction was correct, and the player never notices the check even occurred. If they don't match — because another player's action hit you, because of a collision the client didn't know about yet, because of packet loss, or because of an anti-cheat correction — the client snaps its current position to the server's authoritative value, then replays every input in its history buffer newer than that sequence number, in order, on top of the corrected position. That replay is what rebuilds an up-to-date, corrected prediction without the client ever having to sit and wait for the network again — and it's also exactly what produces the visible little 'snap back' correction you've probably seen in a laggy online match.

A Real Simulation: Predicted vs Corrected Position, Step by Step

Description is cheap; here's an actual 1D simulation of this, in Python, that we wrote and ran. A 'client' moves at a constant speed for 8 ticks at a 20-tick-per-second rate (0.05s per tick). Network delay is simulated as a fixed 3-tick (150ms) one-way delay in both directions. At server-processing tick 6, the server applies a knockback of -2.0 units that the client had no way to predict — standing in for something like another player's attack landing, which the server knows about before this client does. Here is the actual code that ran:

DT = 0.05                 # seconds per tick (20 Hz)
SPEED = 4.0                # units/sec while input is held
NETWORK_DELAY_TICKS = 3    # one-way delay, i.e. 150ms at 20 ticks/sec
INPUT_TICKS = 8             # client only holds the input for the first 8 ticks
KNOCKBACK_AT_SERVER_TICK = 6
KNOCKBACK_AMOUNT = -2.0
EPSILON = 1e-9

server_position = 0.0
client_position = 0.0
history = {}
in_flight_to_server = []
in_flight_to_client = []

tick = 0
last_input_seq = INPUT_TICKS - 1
while True:
    event = ""

    # 1. Client predicts: apply input locally right away, remember it.
    if tick < INPUT_TICKS:
        input_amount = SPEED * DT
        client_position += input_amount
        history[tick] = client_position
        in_flight_to_server.append((tick + NETWORK_DELAY_TICKS, tick, input_amount))

    # 2. Server processes any input arriving this tick.
    still_in_flight = []
    for arrival_tick, seq, amount in in_flight_to_server:
        if arrival_tick == tick:
            server_position += amount
            if tick == KNOCKBACK_AT_SERVER_TICK:
                server_position += KNOCKBACK_AMOUNT
            in_flight_to_client.append((tick + NETWORK_DELAY_TICKS, seq, server_position))
        else:
            still_in_flight.append((arrival_tick, seq, amount))
    in_flight_to_server = still_in_flight

    # 3. Client receives any authoritative update arriving this tick.
    still_in_flight_client = []
    for arrival_tick, seq, auth_position in in_flight_to_client:
        if arrival_tick == tick:
            predicted_at_seq = history.get(seq)
            if predicted_at_seq is not None and abs(predicted_at_seq - auth_position) > EPSILON:
                corrected_position = auth_position
                highest_known_seq = max(history.keys())
                for replay_seq in range(seq + 1, highest_known_seq + 1):
                    corrected_position += SPEED * DT
                    history[replay_seq] = corrected_position
                client_position = corrected_position
        else:
            still_in_flight_client.append((arrival_tick, seq, auth_position))
    in_flight_to_client = still_in_flight_client

    tick += 1
    if not in_flight_to_server and not in_flight_to_client and tick > last_input_seq:
        break

And here is the actual, real console output from running it (positions rounded to 2 decimal places by the print formatting):

tick   client_pred  event
   0          0.20
   1          0.40
   2          0.60
   3          0.80
   4          1.00
   5          1.20
   6          1.40  server applies unpredicted knockback (-2.0) ack seq 0 confirmed (no correction needed)
   7          1.60  ack seq 1 confirmed (no correction needed)
   8          1.60  ack seq 2 confirmed (no correction needed)
   9         -0.40  RECONCILE: server says seq 3 should be -1.20, client had predicted 0.80 -> corrected current position to -0.40
  10         -0.40  ack seq 4 confirmed (no correction needed)
  11         -0.40  ack seq 5 confirmed (no correction needed)
  12         -0.40  ack seq 6 confirmed (no correction needed)
  13         -0.40  ack seq 7 confirmed (no correction needed)

Final client position: -0.4
Final server position: -0.4
Do they match once every packet has settled? True

Walking through what actually happened: the client predicted a smooth climb to 1.60 by tick 7, unaware of anything else. The server, delayed by 3 ticks, processed the input from sequence 3 at server tick 6 and applied the knockback there, landing on -1.20 for that sequence instead of the 0.80 the client had guessed. When that correction reached the client at tick 9 (3 more ticks later), the client snapped to -1.20 and replayed sequences 4 through 7 on top of it — each replayed input adding back the same 0.20 it always would have — landing on a corrected -0.40. The client's later acks for sequences 4-7 all arrive and match what the client already recomputed during the replay, so no further correction happens. By the time every in-flight packet has settled, client and server agree exactly: -0.4 on both sides.

One honest note on process: an earlier version of this exact test ended the simulation on a fixed tick count, before the last couple of corrections had time to travel back over the simulated network. That made it look, falsely, like the client and server never agreed at all. The fix wasn't in the reconciliation logic — it was in the test itself, which needed to keep running until every in-flight packet had actually settled before comparing final positions. It's a useful reminder for testing anything asynchronous: if a check on your own code gives a surprising result, verify by hand whether the check itself is even measuring a settled state before you trust it.

Lag Compensation: The Other Half of the Problem

Client-side prediction covers your own movement. It says nothing about the much harder problem of hitting other players, whose positions you're seeing on a delay too. If you see an enemy on your screen and fire, what you're aiming at is where that enemy was some number of milliseconds ago, by the time their movement update traveled to you, not where they are on the server right now.

The standard fix, popularized publicly by Valve's networking documentation for the Source engine and used broadly across the industry since, is server-side rewind, usually called lag compensation. The server keeps a short history of every player's position at each recent tick. When it receives a 'shot fired' message from a client, it knows approximately how far behind that client's view of the world was, so it temporarily rewinds its own record of every other player back to what they looked like at that exact moment, checks the hit against that rewound state, and only then applies the result to the current, present-time authoritative state. This is the actual mechanism behind the frustrating experience of getting shot after you'd already ducked behind cover on your own screen: the server isn't being unfair, it's validating the shot against where you actually were from the shooter's perspective, not where you are now.

Why Not Just Trust the Server for Everything?

It's worth being clear that prediction and reconciliation are a deliberate trade-off, not a free upgrade. A fully server-authoritative design with no client prediction at all is simpler to build and harder to cheat, and it's a completely reasonable choice for genres where a few hundred milliseconds of input delay doesn't hurt the experience — most turn-based and strategy games don't bother with any of this. At the opposite extreme, trusting the client's own reported outcome (no server checking at all) is simple and fast but trivially exploitable — speed hacks and position teleports cost an attacker nothing against a server that just believes whatever the client says.

Client prediction plus server reconciliation plus lag compensation is the middle path the industry settled on because it delivers both properties that matter for fast-paced multiplayer games at once: input feels instant locally, and the server never actually believed anything the client didn't subsequently earn through its own independent simulation. The complexity cost — history buffers, replay logic, rewind windows — is the price for getting both, and it's why netcode has a reputation as one of the harder problems in game engineering.

Frequently Asked Questions

Why do I sometimes get snapped back a step in online games? That's server reconciliation correcting a misprediction. The server's authoritative result for a past input differed from what your client had guessed, so the client snapped to the server's value and replayed its newer inputs on top of it.
Is client-side prediction the same thing as lag compensation? No. Prediction is about making your own character's movement feel instant. Lag compensation is a separate mechanism the server uses to fairly judge whether your shot hit another player, given that you were seeing their position on a delay.
Can client-side prediction be exploited to cheat? The prediction itself isn't the exploit surface, because the server re-simulates every input independently and never trusts a client's claimed final position. Cheats that do work against these systems usually target a different layer entirely, like reading another player's position out of client memory for a wallhack.
Does every multiplayer game need client-side prediction? No. It matters most for games where input timing is felt directly, like shooters, fighting games, and fast platformers. Turn-based and slower-paced multiplayer games often skip it entirely and stay fully server-authoritative, since a bit of round-trip delay isn't noticeable there.
What happens to reconciliation if a network packet is lost, not just delayed? The same mechanism still applies. A lost input simply never advances that sequence on the server; once later inputs do arrive, the server processes them from its actual last known state, and the client's next reconciliation snaps to whatever really happened.