Engineering Atlas › Low-Level Design

Game loop: update, render, repeat

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

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 are

If 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

  1. Displays refresh at fixed rates that vary by device, and CPUs vary in speed, so the simulation cannot be tied to either.
  2. Numerical integration and collision detection are only stable and reproducible with equal, small steps, so the simulation runs on a fixed step.
  3. Real time does not arrive in neat multiples of the step, so an accumulator banks the remainder, and interpolation hides the mismatch on screen.
  4. A machine that cannot keep up would otherwise fall further behind each frame, so the loop clamps and caps, trading accuracy for liveness.
  5. 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

OptionStrengthsWeaknessesChoose when
Update per frame, no timingSimplest possibleSpeed depends on hardwareFixed hardware, or a prototype
Variable timestep (dt)Smooth; easy to writePhysics unstable at large dt; not reproducibleSimple games without physics or replays
Fixed step, no interpolationDeterministic; simpleStutter when display and step rates differTurn-based or low-rate simulations, servers
Fixed step with accumulator and interpolationDeterministic and smoothOne step of visual latency; two copies of stateMost real-time games and simulations
Fixed step with extrapolationNo added latencyVisual glitches at collisionsFast-paced games that tolerate small artifacts

Pitfalls in production

Leadership lens

Check yourself

  1. 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.

  2. 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.

  3. 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.

  4. 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

Free resources

Open in the interactive app: practice, quiz, mock interview →