Quick answer: set_timer fires on frame boundaries, accumulating sub-frame error. For long-running schedules, track elapsed time against a monotonic clock and correct.

A survival game spawns a wave every 30s via set_timer. After an hour, waves are noticeably out of sync with the displayed countdown — small per-tick rounding compounded.

Why It Drifts

set_timer posts an event every N milliseconds, but the event is only processed on the next frame boundary. At 60 FPS, that’s up to ~16ms of slop per fire. Over thousands of fires, it adds up.

Clock-Anchored Scheduling

import pygame

next_wave_at = pygame.time.get_ticks() + 30000

def update():
    global next_wave_at
    now = pygame.time.get_ticks()
    if now >= next_wave_at:
        spawn_wave()
        next_wave_at += 30000   # anchor to the schedule, not "now"

Advancing next_wave_at += 30000 (not = now + 30000) keeps the schedule anchored. Drift never accumulates.

Catch-Up for Long Pauses

while now >= next_wave_at:
    spawn_wave()
    next_wave_at += 30000

If the game was paused or hitched, the while loop fires any missed waves. Optional — for some games you’d skip instead.

Display the Same Source

Compute the countdown UI from next_wave_at - now so the display and the spawn use one clock. They can never disagree.

Verifying

Run a long session (or fast-forward via a debug speed multiplier). Wave N fires at exactly 30N seconds. Countdown matches the spawn.

“Anchor schedules to absolute time, not to ‘now + interval’. That one change kills drift.”

For anything competitive or speedrun-timed, also use time.get_ticks() consistently — never mix it with frame-count-based timing.