Why Your Source Engine Weapon Feels Laggy — and How Prediction, Interpolation, and Lag Compensation Fit Together
A laggy custom weapon in a Source mod usually isn't a weapon bug — it's a misunderstanding of prediction, interpolation, and lag compensation. Here's how the three systems fit together and how to write networked fire code that feels instant.
05 Oct 2025, 07:22 UTC

You've written a custom weapon for your Source mod. On a local server it fires instantly. Then a friend joins with 80 ms of ping, and suddenly the muzzle flash appears late, the hit registers a beat after the click, and the whole thing feels like it's firing through mud. Nothing is wrong with your weapon code — the problem is that you wrote it as if the client and server share one timeline. They don't, and Source's entire multiplayer feel comes from three systems that paper over that gap: client-side prediction, entity interpolation, and lag compensation.
The thesis of this post: if you understand what each of those three systems is responsible for, the "correct" way to write networked weapon code stops being mysterious. You run shared, prediction-aware code on both sides, let the server stay authoritative, and stop trying to outsmart the networking layer.
The authoritative server problem
Source uses a strict client-server model. The client doesn't simulate the world; it sends user commands (CUserCmd — essentially "these buttons, this view angle, this tick") to the server, and the server sends back snapshots of world state at its tick rate. If your weapon logic only ran on the server, every click would take a full round trip before anything visibly happened. At 80 ms ping that's an eternity for a hitscan rifle.
Prediction: make the local player feel instant
Client-side prediction runs the same movement and weapon code on the client that the server will run, using the client's own inputs, immediately. Your view model raises, the sound plays, the recoil kicks — all before the server has even seen the command. When the server's authoritative result arrives, the client compares it against what it predicted. If they match, nothing happens. If they don't, you get a misprediction correction — the rubber-band snap every Source player knows.
The practical rule for modders: any code that affects the local player's immediate experience must live in shared code that can run under the prediction system (in SDK terms, code guarded so it executes in both the predicted client context and on the server). A common beginner mistake is firing effects only in server-side code. That "works" but reintroduces the full round-trip delay you were trying to avoid.
Interpolation: remote players are always in the past
Remote entities can't be predicted — you don't know what another player will do. Instead, the client buffers incoming snapshots and renders remote entities slightly behind real time, interpolating between the last two snapshots so motion looks smooth. The cl_interp family of cvars controls how far behind you render. Set interpolation too low and packet loss turns into visible jitter and teleporting; set it too high and you're shooting at ghosts from 150 ms ago. This is a genuine trade-off, not a setting with a "best" value — it depends on tick rate and how lossy your players' connections are.
Lag compensation: letting you aim at what you see
Here's the subtle piece. Because of interpolation, when you shoot at an enemy on your screen, you're aiming at where they were, not where the server thinks they are now. With sv_lagcompensation enabled, the server rewinds entity positions by roughly the shooter's latency plus interpolation delay when processing a hitscan shot, then tests the hit against that rewound state. That's why you can aim directly at a visible target instead of leading by your ping.
The cost: this opens abuse vectors. Fake-lag and "backtrack" cheats deliberately manipulate their reported timing to stretch the rewind window. That's why competitive titles ship convar limits and server-side sanity checks on how far back a rewind is allowed to go. If you're building anything competitive, treat unbounded lag compensation as an exploit, not a feature.
A worked example: the hitscan rifle done right
Suppose your rifle's PrimaryAttack() currently plays the sound and spawns the tracer only in server code. The fix follows the standard Source pattern:
- Move the fire logic into shared code so it runs inside the client's prediction pass and again on the server.
- Guard one-time effects appropriately: predicted effects (view model animation, local sound) play on the client immediately; authoritative effects (damage, the tracer other players see) come from the server.
- Let the server's hit registration run with lag compensation active, so the rewind handles the shooter's latency — don't try to compensate manually in weapon code.
- Test under artificial latency. On a listen server, use
net_fakelagto add delay, watchnet_graph, and usecl_showerrorand the prediction debug overlays to confirm your weapon produces no persistent mispredictions. Rubber-banding or constantly correcting view punch means your client and server code paths have diverged — find the divergence rather than suppressing the error.
Run these commands in the developer console on your own listen server; they only affect your local test session, so there's nothing to roll back.
The honest trade-off
Prediction and lag compensation are feel optimizations that deliberately show players a slightly fictional world. Every millisecond of smoothness you grant is a millisecond of state the server has to trust or rewind, and cheaters live in exactly that gap. Also, a hard caveat: cvar names, defaults, and available internals differ across Source branches (2007/2013 SDK, CS:GO's branch, Source 2), and the public 2013 SDK omits some engine internals Valve's own games use. Verify names and behavior against the headers in your branch — prediction.h is a good starting point — before copying values from a forum post about a different game.
What to do next
Pick one weapon in your mod, run it under net_fakelag 100, and watch net_graph and the prediction errors while you fire. If it feels instant and produces no mispredictions, your shared-code split is right. If not, you now know which of the three systems to blame — and that's most of the battle.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.