Why your game runs faster at 144Hz: fixed timestep game loops and interpolation
Per-frame movement speeds up on high refresh screens. Fix it with a fixed timestep and accumulator, interpolate when drawing, cap the spiral of death. 60/144 Hz math.
- Moving things per frame ties game speed to the display; at 144Hz it runs 2.4 times faster than at 60Hz
- Multiplying by delta time fixes speed, but physics still differs with frame rate, so jumps reach different heights
- A fixed timestep with an accumulator steps physics identically on every machine, and interpolation keeps the picture smooth
- Cap the frame time or the step count, or a slow machine falls into the spiral of death
A game tested on 60Hz monitors for a month, then a player opens it on a 144Hz screen: the character sprints out of control, or clears a gap that was meant to be impossible. The cause is a game loop that ties movement to the number of frames. This article goes from that bug to the fixed timestep Glenn Fiedler described in “Fix Your Timestep!” (2004), then looks at how Unity, Godot and Roblox expose the same idea.
Numbers in this article were computed with Node.js 20.14; engine details checked against the Unity 6.6, Godot 4.7 and Roblox Creator documentation as of October 2026.
The first bug: moving per frame
function frame() {
player.x += 5; // 5 px per frame
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame fires at the display’s refresh rate. MDN says 60Hz is most common but 75, 120 and 144Hz are widely used, and warns plainly that animations not based on elapsed time will run faster on high refresh-rate screens.
| Display | Frames per second | Actual speed |
|---|---|---|
| 60 Hz | 60 | 5 × 60 = 300 px/s |
| 144 Hz | 144 | 5 × 144 = 720 px/s |
720 / 300 = 2.4 times faster. The reverse holds too: a machine that drops to 30 fps plays at half speed.
Half a fix: multiply by delta time
The fix everyone reaches for first: measure the elapsed time (dt) and multiply, player.x += 300 * dt. Speed is now the same on every screen. But physics with acceleration is still off, because integrating step by step gives results that depend on the step size.
Worked example: jumps of different heights
A jump with initial velocity 600 px/s and gravity 1,800 px/s², using semi-implicit Euler (update velocity first, then position), which is how most game code is written:
vy -= 1800 * dt;
y += vy * dt;
Real physics puts the apex at v²/2g = 600² / 3,600 = 100 px. In the game the apex is the sum of the steps. With N steps until velocity reaches 0 (N = 600 / (1800·dt)):
height = dt × (N × 600 − 1800·dt × N(N+1)/2)
| Frame rate | dt | Steps N | Arithmetic | Apex |
|---|---|---|---|---|
| 30 Hz | 1/30 | 10 | (6,000 − 60×55) / 30 | 90.0 px |
| 60 Hz | 1/60 | 20 | (12,000 − 30×210) / 60 | 95.0 px |
| 144 Hz | 1/144 | 48 | (28,800 − 12.5×1,176) / 144 | 97.9 px |
Put a 96 px ledge in a level: the 144Hz player makes it, the 60Hz player does not. Same game, same level, dt multiplied on every line. When frames hitch and dt varies, results get more random still, and one large dt is enough for a fast object to tunnel through a thin wall.
Multiplying by dt fixes speed, not determinism. Physics has to take steps of the same size every time.
Fixed timestep with an accumulator
Fiedler’s idea: the display produces time, the physics consumes it in fixed-size chunks, and whatever is left over waits in a bucket (the accumulator) for the next frame. Physics always steps with dt = 1/60, whatever the refresh rate, so the jump reaches 95.0 px on every machine.
60 Hz physics on a 144 Hz display: each frame adds 1/144 s = 6.94 ms; each step consumes 16.67 ms.
| Frame | Bucket after adding | Steps | Left over (alpha) |
|---|---|---|---|
| 1 | 6.94 ms | 0 | 0.42 |
| 2 | 13.89 ms | 0 | 0.83 |
| 3 | 20.83 ms | 1 | 0.25 |
| 4 | 11.11 ms | 0 | 0.67 |
| 5 | 18.06 ms | 1 | 0.08 |
| 6 | 8.33 ms | 0 | 0.50 |
Twelve display frames = 1/12 s = exactly 5 physics steps (60 / 12 = 5), an average of 0.42 steps per frame. Most frames run no physics at all. On a 30 Hz display it flips: every frame runs 2 steps.
Interpolation: no stutter
New problem: drawing physics positions directly on a 144 Hz screen makes objects sit still for 2-3 frames and then jump, because physics changes only 60 times a second. The fix is to keep the previous state (prev) and draw somewhere between prev and curr, according to what is left in the bucket:
alpha = accumulator / dt, then draw at prev × (1 − alpha) + curr × alpha
In frame 2 of the table alpha = 0.83, so the picture sits 83% of the way from the previous state to the latest. The cost is drawing up to one step behind the simulation (16.7 ms at 60 Hz). For rotations, Fiedler recommends quaternion slerp rather than a plain lerp.
The spiral of death, and the cap
If computing one step takes longer in real time than the time it simulates, say 20 ms per step for a 16.7 ms dt, the bucket grows every frame. The next frame must run more steps, which makes it slower, which means even more steps, until the game freezes. That is the spiral of death.
The fix is a cap, accepting that the game slows down instead of locking up:
- Fiedler clamps frame time to 0.25 s; at 60 Hz that is at most 0.25 × 60 = 15 steps per frame
- Godot’s
max_physics_steps_per_framedefaults to 8; below 60 / 8 = 7.5 fps the game visibly slows - Unity’s Maximum Allowed Timestep defaults to 1/3 of a second; time beyond it is simply not simulated
The whole loop (JavaScript)
const DT = 1 / 60; // physics at 60 Hz
const MAX_FRAME = 0.25; // cap against the spiral of death
let acc = 0, last = 0;
let prev = { x: 0 }, curr = { x: 0 };
function update(s, dt) { // pure physics, always a fixed dt
return { x: s.x + 300 * dt };
}
function frame(nowMs) {
const now = nowMs / 1000;
const frameTime = Math.min(now - last, MAX_FRAME);
last = now;
acc += frameTime;
while (acc >= DT) { // consume time in fixed chunks
prev = curr;
curr = update(curr, DT);
acc -= DT;
}
const alpha = acc / DT; // remainder = how far between two steps
draw(prev.x * (1 - alpha) + curr.x * alpha);
requestAnimationFrame(frame);
}
requestAnimationFrame(t => { last = t / 1000; requestAnimationFrame(frame); });
draw(x) is your game’s drawing function. Feeding frame simulated timestamps 144 times over one second gives curr.x = 300 exactly and a drawn position of 295, one step behind (300 × 1/60 = 5 px), as expected. MAX_FRAME covers another case too: browsers pause requestAnimationFrame in background tabs, so on return now - last can be minutes. Without a cap the game tries to simulate all of it in one frame.
How the engines expose it
| Engine | Physics / fixed step | Every frame | Smoothing |
|---|---|---|---|
| Unity | FixedUpdate every Time.fixedDeltaTime (the manual’s example: 0.02 = 50 per second) | Update | Rigidbody.interpolation (off by default) |
| Godot | _physics_process(delta) at physics_ticks_per_second (default 60) | _process(delta) | Project Settings > Physics > Common > Physics Interpolation |
| Roblox | engine-run physics; PreSimulation before, PostSimulation after | PreRender (client only) | handled by the engine |
Details worth knowing:
- Unity: when the frame rate is above the fixed rate, some frames get no
FixedUpdate; below it, some frames get two or three. Exactly the bucket table above - Godot: movement belongs in
_physics_process. With physics interpolation on, moving objects outside the physics tick causes jitter; callreset_physics_interpolation()when teleporting - Roblox:
SteppedandRenderSteppedcarry migration notes pointing toPreSimulationandPreRenderfor new work, andHeartbeatfires at the same point asPostSimulation(after physics). Every event passes a delta time; always multiply by it. There is alsoRunService:BindToSimulation(), which calls a function at a fixed frequency independent of frame rate (30 Hz by default), but only whenWorkspace.UseFixedSimulationis enabled
-- Roblox: move an Anchored part at the same speed on every screen
local RunService = game:GetService("RunService")
local part = workspace.MovingPlatform
local SPEED = 8 -- studs per second
RunService.PreSimulation:Connect(function(dt)
part.CFrame += Vector3.new(SPEED * dt, 0, 0)
end)
If you are learning Roblox, EP5 Lava Rising applies the same rule to lava that rises over time.
Common mistakes
- Moving per frame without dt. The original bug, invisible if the whole team uses 60Hz monitors. Test on a high refresh screen, or cap the frame rate to 30
- Reading input inside the fixed step. A key pressed for a single frame can land in a frame with no step at all (frames 1, 2 and 4 in the table). Collect input every frame and let the next step consume it
- Moving physics objects in Update / _process. Physics and rendering fight each other: jitter, or wrong collisions
- No cap. One hitch (asset load, tab switch) is enough to start the spiral of death
- Starting with a high physics rate. 240 Hz runs four times as many steps as 60 Hz. Start at 60 and raise it when you actually see tunnelling; in Godot, raise
max_physics_steps_per_framewith it
Sources
- Fix Your Timestep! — Glenn Fiedler, Gaffer On Games
- Window.requestAnimationFrame() — MDN
- Engine (physics_ticks_per_second, max_physics_steps_per_frame) — Godot Docs
- Using physics interpolation — Godot Docs
- Fixed updates — Unity Manual and Handling variation in time
- Rigidbody.interpolation — Unity Scripting API
- RunService — Roblox Creator Docs
FAQ
Do I need to write the accumulator myself in Unity, Godot or Roblox?
No, the engine does it. Your job is putting code in the right place, physics and movement in FixedUpdate (Unity) or _physics_process (Godot). In Roblox, always multiply by the deltaTime each event passes you.
What physics rate should I use?
60 Hz is a good start, and it is Godot's default. Games with very fast objects may need more so they do not tunnel through walls, but every extra Hz costs CPU, and a much higher rate also needs a higher cap on steps per frame.
Does interpolation make the picture lag?
By at most one step, because it draws between the previous and the latest state. At 60 Hz that is at most 16.7 ms. Most games cannot feel it, and the trade is a picture that does not stutter.
Commands and settings were tested on sample systems. Try them on a test machine before production.