Source Engine Networking: Lag Compensation and Interpolation Architecture
An architecture note on Source Engine's authoritative-server networking: how lag compensation rewinds entity positions for hitscan validation and why the interpolation buffer is a deliberate tradeoff.
05 Jul 2025, 21:29 UTC

Requirements: Authoritative Simulation with Fair Hit Registration
Valve's Source engine targets competitive multiplayer where the server must remain the single source of truth for game state while delivering responsive movement and fair hit registration across connections ranging from 20 ms to 200 ms latency. The core requirements are:
- Server authority: All gameplay-critical state (positions, health, ammo, ability cooldowns) is simulated exclusively on the server.
- Client prediction: Local player movement must feel instant regardless of round-trip time.
- Lag compensation: Hitscan weapons (instant-trace firearms, melee traces) must register hits based on what the shooter saw, not on where entities actually are when the packet arrives.
- Visual continuity: Remote entities must move smoothly between discrete server snapshots without stutter or teleportation.
The smallest suitable design that satisfies all four is the engine's built-in snapshot, interpolation, prediction, and lag-compensation pipeline. Building custom netcode on top of Source's networking layer is almost always a mistake—it duplicates complexity, fights the engine's assumptions, and introduces new trust-boundary violations.
Trust and Data Boundaries
The server is the only trust boundary. Clients send UserCmd structures (view angles, movement buttons, attack flags, tick count) each tick. The server:
- Validates command sequence numbers and clamps rates via
sv_maxcmdrateandsv_mincmdrate. - Runs the same movement simulation the client predicted.
- Rewinds other entities to the shooter's render time when processing hitscan attacks.
- Emits a snapshot (entity positions, animation state, effects) at
sv_updaterateHz.
Clients receive snapshots, buffer them for cl_interp seconds (default 0.1), then interpolate between the two most recent snapshots for rendering. The client's predicted local player is corrected when the authoritative snapshot arrives; corrections are smoothed to avoid visible pops.
Never trust client-reported: hit positions, damage amounts, entity indices hit, timestamps, or "I was behind cover" claims. All validation happens server-side during the rewind window.
Lag Compensation Mechanics
When the server processes a hitscan attack, it calculates the shooter's view time:
view_time = server_tick_time - lerp_time - latency
where lerp_time = cl_interp + cl_interp_ratio / cl_updaterate. The server then rewinds every other entity to its position at view_time using the entity's stored position history (typically 1 second of backtrack). The trace runs against these rewound positions.
This design trades server memory (position history per entity per tick) and CPU (rewind + trace per shot) for fairness. The maximum rewind window is bounded by sv_maxunlag (default 1.0 second in most branches). Players with latency exceeding this window cannot be fully compensated—their shots test against entity positions newer than what they saw.
Entity Interpolation Buffer
Remote entities are rendered at render_time = snapshot_time + cl_interp. The default cl_interp 0.1 (100 ms) means:
- Clients see other players 100 ms "in the past" relative to the latest snapshot.
- Packet loss or jitter up to ~100 ms is absorbed without extrapolation.
- Input-to-display latency for remote entities increases by the buffer amount.
Competitive titles often reduce this via cl_interp_ratio 1 and high cl_updaterate (e.g., 100 Hz → 10 ms lerp). The tradeoff: lower buffer = more extrapolation pops under loss. cl_interp_ratio scales the buffer relative to update interval; cl_interp sets a floor.
Operational Checks: What to Monitor
| Metric | Healthy Range | Degraded Symptom | Diagnostic Command |
|---|---|---|---|
| Server frame time | < tick interval (e.g., < 15.6 ms at 66 tick) | Snapshot delay, choke spikes, rubber-banding | stats in server console |
| Client choke | 0–1 packets per cl_updaterate interval | Stutter, delayed hit reg, interpolation pops | net_graph 3 on client |
| Client loss | 0% | Extrapolation, teleport corrections | net_graph 3 |
| Server tickrate stability | Constant (66, 100, etc.) | Variable simulation step → inconsistent rewind | sv_showtickrate 1 |
Run these on a listen server or dedicated server with sv_cheats 1 (required for stats, sv_showtickrate). net_graph works on any client. Choke > 2 consistently means the client's rate or cl_updaterate exceeds the connection's capacity—lower them before blaming hit registration.
Failure Modes and Tuning
Extrapolation Popping
When snapshots arrive late or are lost, the client extrapolates entity motion using the last known velocity. Sudden direction changes cause visible pops when the next snapshot corrects the position. Mitigation: increase cl_interp or cl_interp_ratio; ensure server sv_updaterate ≥ client cl_updaterate.
Rewind-Window Abuse
High-ping players (150+ ms) gain a marginal advantage: their shots rewind further, potentially hitting players who have already moved to cover on their own screens. This is inherent to shooter-favoring compensation. Mitigation: reduce sv_maxunlag (e.g., to 0.5) if the design prioritizes defender fairness over shooter fairness. Test with net_fakelag 200 on a listen server to feel the effect.
Hit-Registration Complaints Beyond Compensation Limits
When latency > sv_maxunlag, the server cannot rewind far enough. The shot tests against entity positions newer than the shooter's view. Result: blood decals without damage, or "I shot first" deaths. No console variable fixes this—only network improvements or design acceptance.
Concrete Configuration Example: Competitive 100-Tick Setup
For a 100-tick competitive server:
// Server.cfg
sv_maxupdaterate 100
sv_minupdaterate 66
sv_maxcmdrate 100
sv_mincmdrate 66
sv_maxrate 0 // 0 = unlimited
sv_minrate 196608 // ~1.5 Mbps floor
sv_maxunlag 0.5 // Tighter rewind window
// Client autoexec or launch options
cl_updaterate 100
cl_cmdrate 100
rate 786432 // ~6 Mbps
cl_interp_ratio 1
cl_interp 0 // Floor; actual lerp = ratio/updaterate = 10 ms
Run on the server with rcon or in server.cfg. Clients apply via autoexec.cfg or launch options. Verify with net_graph 3: lerp should read ~10 ms, choke 0, loss 0.
Risk: Clients on poor connections will choke at 100 updaterate. Provide a fallback config (66 tick, ratio 2, interp 0.03) and detect via net_choke monitoring.
Verification Procedure
- Start a listen server:
map [map_name]withsv_cheats 1. - Enable
net_graph 3andsv_showtickrate 1. - Add artificial latency:
net_fakelag 100(client) or use a traffic shaper. - Fire hitscan weapon at moving bot; note hit registration with
sv_showlagcompensation 1(shows rewind boxes). - Disable lag compensation:
sv_lagcompensation 0; repeat shot. Compare hit reliability. - Load server with bots; watch
statsfor frame time exceeding tick interval.
If frame time spikes, the simulation is overrunning—optimize entity think functions, reduce entity count, or lower tickrate before tuning network variables.
Conditions That Would Change This Design
- Projectile-only combat: If no hitscan weapons exist, lag compensation can be disabled (
sv_lagcompensation 0), saving CPU and removing rewind-window abuse. - Deterministic rollback networking (GGPO-style): Requires full state serialization and rollback framework—Source's snapshot model is not this. Migration means rewriting the networking layer.
- Client-authoritative movement (e.g., VR, some co-op): Would invert the trust boundary; Source's prediction/correction model fights this.
- Very high entity counts (1000+ networked entities): Position history memory scales linearly. Profile
CBaseEntity::GetLaggedPositionstorage; consider reducingsv_maxunlag.
Limitations and Review Notes
Exact console variable names, defaults, and compensation limits differ between Source branches (Source 2013, CS:GO, etc.). The values above reflect common lineages. Verify against your specific SDK branch. Source is largely closed-source; some behaviors (exact rewind interpolation, animation state handling) are observable but not documented. Treat this article as a starting point for testing, not a specification.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.