Quick answer: clock.tick_busy_loop() spins the CPU continuously. clock.tick() yields via sleep. For most games at 60 FPS, switch to clock.tick(60) and CPU usage drops to near zero. For tear-free rendering on multiple monitors, set vsync=1 on display init.

Here is how to fix Pygame games that pin a CPU core to 100% even though they run at a comfortable 60 FPS. Your laptop fan spins up just from running the game; battery drains in an hour. The cause is almost always clock.tick_busy_loop instead of clock.tick, or omitting vsync from the display setup.

The Symptom

Your game caps FPS to 60 successfully — the frame counter shows steady 60 — yet Activity Monitor / Task Manager shows the Python process using 100% of one CPU core. The fan is loud. Battery drains fast.

What Causes This

tick_busy_loop spins. It compares system time against the target frame time in a tight loop, never yielding to the OS. CPU is fully utilized for what amounts to waiting.

tick(0) with no cap. Calling tick with 0 means “no FPS limit”. Without other idle behavior, the game runs as fast as possible.

No vsync. Without vsync, presentation does not wait for monitor refresh. CPU and GPU race ahead.

OS sleep granularity. On Windows, the default timer resolution is 15.6ms, so time.sleep(0.001) can sleep up to 15ms. tick still does the right thing but Windows may reduce timer resolution causing slightly worse jitter than expected.

The Fix

Step 1: Use Clock.tick.

import pygame

pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Game logic and draw...
    pygame.display.flip()
    clock.tick(60)   # OS-friendly cap

tick(60) uses time.sleep internally to wait for the next frame. CPU is mostly idle between frames.

Step 2: Enable vsync for tear-free output.

screen = pygame.display.set_mode(
    (1280, 720),
    flags=pygame.SCALED,    # required for vsync in some pygame versions
    vsync=1
)

while running:
    # ...
    pygame.display.flip()
    clock.tick(0)    # vsync handles the cap

vsync limits FPS to monitor refresh rate. Combined with clock.tick(0), the loop blocks at flip() until the next refresh.

Step 3: Use tick_busy_loop only when needed. For rhythm games or precise frame-locked simulations where every millisecond of jitter matters, tick_busy_loop is correct. Document why you chose it and accept the CPU cost.

Step 4: On Windows, raise timer resolution if needed. If you observe choppy frame pacing with tick, raise the OS timer:

import ctypes
ctypes.windll.winmm.timeBeginPeriod(1)   # 1 ms timer resolution
# ...game loop...
ctypes.windll.winmm.timeEndPeriod(1)

Use sparingly — raising timer resolution affects every process on the system and increases overall power use.

Step 5: Profile to confirm.

import time

while running:
    start = time.perf_counter()
    # game logic
    pygame.display.flip()
    work_ms = (time.perf_counter() - start) * 1000
    clock.tick(60)
    print(f"Work: {work_ms:.1f}ms, FPS: {clock.get_fps():.0f}")

If work_ms is consistently small (e.g., 2ms) and FPS is 60, your game is mostly idle. The CPU should reflect that.

Battery-Friendly Defaults

For laptops and Steam Deck, default to clock.tick(60) with vsync. Drop FPS cap to 30 in a power-saver setting. Avoid tick_busy_loop unless gameplay specifically demands it. Players notice fan noise and battery drain more than 1ms of frame jitter.

“tick yields. tick_busy_loop spins. Pick the one that matches your power and precision tradeoff.”

Related Issues

For frame-time drift, see Clock.tick Drifting. For inconsistent FPS, see Clock.tick Inconsistent FPS.

tick(60) by default. vsync for monitors. tick_busy_loop only when you measure that you need it.