Game loop: update, render, repeat
Open in the interactive app: practice, quiz, mock interview →In one minute
- A game loop is the one loop that owns time in an interactive simulation: each pass reads input, advances the world, and draws a frame, and it never waits for the user the way an event-driven app does.
- The core decision is how simulated time relates to wall-clock time. Tying the update to the CPU speed makes the game run faster on faster machines; passing a variable delta time fixes the speed but makes physics depend on the frame rate.
- The robust form is a fixed timestep with an accumulator: real elapsed time is banked, the world advances in equal steps of, say, 10 ms, and rendering interpolates between the last two states using the leftover fraction.
- A slow machine can fall behind forever (the "spiral of death"), so the loop clamps how much time one frame may add and caps the steps per frame, deliberately dropping time instead of drowning.
- The same shape runs far outside games: control loops in robots, Kubernetes reconcilers, trading simulators and audio callbacks all separate "advance the model by a fixed step" from "observe the model".
How it works
The problem
Most applications sleep until something happens: a click, a request, a timer. A game, a physics simulation or a robot controller cannot. The ball keeps moving whether or not the player touches the keyboard, so something must keep advancing the world. That something is a loop, and the interesting part is not the loop itself but how it keeps time.
The first version everyone writes looks like this:
while running:
handle_input()
update() # move every object by a fixed amount
render()On the developer's laptop it runs at 200 passes per second and feels right. On a gaming PC it runs at 900 and everything moves 4.5 times faster; on an old phone it runs at 30, in slow motion. Only fixed hardware, like old arcade boards, can get away with this.
Variable timestep
The natural fix is to measure how long the last frame took and scale movement by it:
last = clock()
while running:
now = clock()
dt = now - last
last = now
update(dt) # position += velocity * dt
render()Speed is now right on every machine, and for simple games this is fine. The trouble appears with physics. Integration with a large dt is less accurate and can become unstable: a spring that settles at 60 updates per second can explode at 15, and a bullet moving 2 meters per step can jump over a 1-meter wall. Worse, two machines given the same inputs compute different worlds, because they sliced time differently. Replays, networked lockstep games and bug reproduction all need the same inputs to produce the same result.
Fixed timestep with an accumulator
The standard answer, popularised by Glenn Fiedler's article "Fix Your Timestep!", decouples the two clocks. The simulation always advances by the same step (for example 10 ms, 100 updates per second). Rendering happens as often as the display allows. Between them sits an accumulator that holds real time not yet simulated:
acc = 0
while running:
now = clock()
acc += min(now - last, MAX_FRAME) # clamp a huge gap
last = now
steps = 0
while acc >= STEP and steps < MAX_STEPS:
update(STEP)
acc -= STEP
steps += 1
render(alpha=acc / STEP) # how far into the next step we areIf a frame took 25 ms, the world advances two 10 ms steps and 5 ms stays in the bank for next time. If a frame took 4 ms, no update runs at all and the bank grows. Over a long run the simulation advances exactly as much as the clock did, in identical slices.
Interpolation
With a 100 Hz simulation and a 144 Hz display, some frames would show the same state twice and motion would stutter. The fix is to keep the previous and current states and render a blend weighted by alpha = acc / step: 5 ms into a 10 ms step, each object is drawn halfway between them. This costs up to one step of visual latency. Extrapolating forward from velocity removes the latency but briefly shows objects passing through walls.
The spiral of death
Suppose one update takes 12 ms of CPU while the step is 10 ms. Each frame banks more time than it can spend, so the next frame needs more updates, which take even longer. The loop never catches up and the game freezes. Two guards stop this. A clamp on elapsed time (often 100 to 250 ms) means a debugger breakpoint or a window drag does not try to simulate the lost seconds. A cap on steps per frame means a persistently slow machine drops simulation time and runs in slow motion rather than locking up. Both deliberately break the "simulated time equals real time" promise, and that is the right call: this is the overload problem shape, and shedding work beats queueing it without bound.
Input and determinism
Input should be sampled once per frame and applied at a specific simulation tick, not mid-update. When inputs are recorded as "at tick 1,204 the player pressed jump", a replay feeds the same inputs at the same ticks and reproduces the game exactly, regardless of the frame rate it is watched at. Real-time strategy games built on deterministic lockstep send only inputs over the network and rely on every client computing the identical world from them; that only works with a fixed step, a seeded random generator, and care with floating point. Multiplayer servers apply the same idea with a server tick rate: shooters commonly run at 30, 64 or 128 ticks per second, and clients interpolate between snapshots exactly as the renderer interpolates between steps.
In browsers, engines and object designs
In a browser you cannot write while (true) without blocking the event loop, so requestAnimationFrame(callback) asks the browser to call you before each repaint with a high-resolution timestamp; the accumulator lives inside that callback. Unity exposes the same split as FixedUpdate (physics, default 0.02 s) and Update (once per rendered frame). In an object design, the loop is the composition root: each entity offers update(step) (the Update Method pattern), the renderer only reads state, and no entity calls the clock. Inject the clock into the loop and a test can run ten minutes of game time in milliseconds.
Reviewing agent-written code
An agent asked to "make the player move" will usually write player.x += 5 inside a requestAnimationFrame callback, tying speed to the monitor, or call Date.now() inside entity code. Ask for one loop that owns the clock, a fixed step, interpolation, a clamp, and inputs keyed by tick, and check that a test can drive it with a fake clock.
The same shape elsewhere
A Kubernetes controller is a game loop without graphics: observe, compare with the desired state, act, repeat. A drone flight controller runs its attitude loop at a fixed 1 kHz or more because the control maths assumes equal time slices, and a trading backtester advances a simulated clock in fixed bars. In every case fixed steps buy stability and reproducibility, and the system needs an explicit policy for falling behind.
Down the stack
Layer 1The contract
A fixed-step loop promises: every call to update receives exactly step; over any run without clamping, the number of updates equals floor(total elapsed time / step); the leftover is always in [0, step) after a frame, so alpha is in [0, 1); inputs scheduled for tick N are applied before update N and never in a different tick; and when clamping or the step cap kicks in, the dropped time is accounted for rather than silently lost. Given the same initial state and the same input schedule, the world after N ticks is the same no matter how the frames were timed.
Layer 2The mechanism
The pieces are an accumulator, a tick counter, two copies of renderable state (previous and current), and an input queue keyed by tick. Engines make this concrete: Unity's Time.fixedDeltaTime is the step and Time.maximumDeltaTime the clamp; physics libraries such as Box2D recommend a fixed step with a fixed number of velocity and position iterations. Large engines often use an entity component system, where update is not a virtual call per object but a system function that iterates over a packed array of components, for example all Position and Velocity pairs. Double buffering the state, either as prev and curr fields or two whole arrays swapped by pointer each tick, gives the renderer a consistent snapshot without locks.
Layer 3The machine
At 60 Hz a frame has a 16.67 ms budget; at 144 Hz, 6.94 ms. The clock read is cheap: clock_gettime(CLOCK_MONOTONIC) goes through the vDSO on Linux in roughly 20 to 50 ns, and performance.now() is similar in browsers, though browsers coarsen it to resist timing attacks. Sleeping is not cheap or precise: Windows defaults to a timer resolution of about 15.6 ms unless a process calls timeBeginPeriod(1), and Linux adds a default 50 us timer slack, so loops that sleep to pace themselves overshoot and stutter. Most games pace on vsync instead.
The update path is where the runtime cost of the pattern shows up. Calling a virtual update() on 10,000 heap-allocated entities costs an indirect call each (a few ns when predicted) but, more importantly, a cache miss each when the objects are scattered, around 80 to 100 ns from DRAM. Packing components into contiguous arrays turns that into sequential reads across 64-byte cache lines that the prefetcher handles well, often a 5 to 10 times speedup. In garbage-collected runtimes, a few allocations per entity per frame add up to megabytes per second, and a 20 ms GC pause is a dropped frame, so loops preallocate and reuse. For lockstep determinism, floating point is the trap: x87 extended precision, fused multiply-add contraction and different sin implementations make machines diverge, which is why some RTS engines simulate in fixed-point integers.
Why it's built this way
- Displays refresh at fixed rates that vary by device, and CPUs vary in speed, so the simulation cannot be tied to either.
- Numerical integration and collision detection are only stable and reproducible with equal, small steps, so the simulation runs on a fixed step.
- Real time does not arrive in neat multiples of the step, so an accumulator banks the remainder, and interpolation hides the mismatch on screen.
- A machine that cannot keep up would otherwise fall further behind each frame, so the loop clamps and caps, trading accuracy for liveness.
- Reproducibility makes replays, networking and debugging possible, so input is keyed by tick and the clock is injected rather than read by entities.
Diagram
flowchart TD
A["frame(now)"] --> B["elapsed = now - last"]
B --> C{"elapsed > max_frame?"}
C -- yes --> D["drop excess, clamp"]
C -- no --> E["acc += elapsed"]
D --> E
E --> F{"acc >= step and steps < cap?"}
F -- yes --> G["apply inputs for tick; update(step); tick++"]
G --> F
F -- "no, still behind" --> H["drop whole steps"]
F -- "no, caught up" --> I["alpha = acc / step"]
H --> I
I --> J["render(prev, curr, alpha)"]Worked example
A browser racing game simulates at 100 Hz (10 ms step) and renders with requestAnimationFrame. On a 60 Hz laptop, frames arrive every 16.7 ms: the loop alternates between one and two updates per frame (averaging 1.67), and alpha cycles through values like 0.67, 0.33 and 0.0. Motion looks smooth because the car is drawn between its last two positions. On a 144 Hz monitor, frames arrive every 6.9 ms, so most frames run zero or one update and interpolation does all the visible work.
Profiling shows an update costs 3 ms on the laptop. On a budget phone it costs 11 ms, longer than the 10 ms step. Without a cap, the phone banks 11 ms of debt per update; after a second the loop is running dozens of updates per frame and the tab freezes. With max_steps = 5 and max_frame = 100 ms, the phone runs five updates per frame, drops the rest, and the race plays at about 80 percent speed. The team then decides the phone needs a cheaper physics mode, a product decision that the cap made visible instead of a hang. Replays record only (tick, input) pairs and reproduce the same lap time at any frame rate; the one desync the team hit was an unseeded Math.random() call in tyre smoke that also nudged the car.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Update per frame, no timing | Simplest possible | Speed depends on hardware | Fixed hardware, or a prototype |
Variable timestep (dt) | Smooth; easy to write | Physics unstable at large dt; not reproducible | Simple games without physics or replays |
| Fixed step, no interpolation | Deterministic; simple | Stutter when display and step rates differ | Turn-based or low-rate simulations, servers |
| Fixed step with accumulator and interpolation | Deterministic and smooth | One step of visual latency; two copies of state | Most real-time games and simulations |
| Fixed step with extrapolation | No added latency | Visual glitches at collisions | Fast-paced games that tolerate small artifacts |
Pitfalls in production
- Speed tied to refresh rate because movement is applied per
requestAnimationFramecall. Fix: move bystepinside a fixed-step loop, never per frame. - Freezes after a breakpoint, a laptop sleep or a dragged window, because the loop tries to simulate the whole gap. Fix: clamp elapsed time per frame.
- Spiral of death on slow devices. Fix: cap steps per frame, record dropped time as a metric, and reduce simulation cost on those devices.
- Replays and lockstep games that desync after minutes. Fix: key inputs by tick, seed every random generator from the match seed, and avoid floating-point operations whose results differ across platforms.
- Entities reading the system clock or
Date.now()directly. Fix: pass time in throughupdate(step)and inject the clock into the loop so tests can drive it. - Stutter from pacing with
sleep. Fix: pace on vsync or the platform's frame callback, and measure with a monotonic clock.
Leadership lens
- Pick the simulation rate early and write it down; physics tuning, network tick rate and replay format all depend on it, and changing it later invalidates all three.
- Track frame time percentiles and dropped simulation time per device class, not average FPS; a p99 frame of 40 ms is what players notice.
- Decide the overload policy explicitly: slow motion, lower simulation detail or fewer entities. The loop's cap is a product decision wearing a code disguise.
- Ask in design review whether the system needs determinism (replays, lockstep networking, reproducible bug reports). If yes, budget for seeded randomness and floating-point discipline from day one.
- The pattern generalises to any control loop your team runs; reconcilers and simulators deserve the same clamp, cap and injected clock.
Check yourself
A game uses a variable timestep and a player reports falling through the floor only when the game hitches. Why?
Show answer
A hitch produces a large
dt, so the player moves far enough in one update to pass through the floor without any overlap being detected. A fixed small step prevents the jump.Why does a fixed-step loop need interpolation, and what does it cost?
Show answer
The display rate rarely matches the step rate, so without blending some frames repeat a state and motion stutters. Interpolating between the previous and current states costs up to one step of visual latency and a second copy of renderable state.
What happens without a step cap when an update takes longer than the step, and what does the cap trade away?
Show answer
Each frame banks more time than it can simulate, so the loop runs ever more updates per frame and freezes. The cap keeps the game responsive by dropping simulated time, so the game runs slower than real time.
Two clients in a lockstep game receive identical inputs but diverge after five minutes. Name two likely causes.
Show answer
Unseeded or shared random number generators, and floating-point results that differ by platform or compiler (FMA contraction, different maths libraries). Inputs applied at different ticks is a third.
Watch
- Overwatch Gameplay Architecture and Netcode (GDC): an entity component system running on a fixed simulation tick, with deterministic prediction and replay of player input.
- I Shot You First: Networking the Gameplay of Halo: Reach (GDC): how a tick-based simulation is shared between hosts and clients under real network conditions.
Free resources
- Fix Your Timestep! (Glenn Fiedler): the accumulator and interpolation technique explained from first principles.
- Game Loop (Game Programming Patterns, Robert Nystrom): the pattern's variants, with clear reasoning about each trade-off.
- Update Method (Game Programming Patterns): how entities plug into the loop, and the pitfalls of mutating the world while iterating it.
- Data Locality (Game Programming Patterns): why packed component arrays beat scattered objects in the update loop.
- Deterministic Lockstep (Glenn Fiedler): what a fixed step and tick-keyed input buy for networked games.
- Anatomy of a video game (MDN): building the loop on
requestAnimationFramein the browser.