Quick answer: Call pygame.event.get() every frame without exception — not just when you need events. Use pygame.event.set_blocked() to prevent high-frequency events like MOUSEMOTION and JOYAXISMOTION from filling the 128-event queue before your keypresses arrive.
Pygame’s event queue is a fixed-size buffer sitting between the operating system and your game loop. The OS delivers input events — mouse moves, key presses, joystick axis updates — into this buffer as they happen. Your game loop is supposed to drain that buffer every frame. When the buffer fills up, new events are silently discarded. No exception is raised. No warning is printed. The player presses Jump and nothing happens because the KEYDOWN event was dropped before your code ever saw it. This is one of the most insidious bugs in Pygame development because the symptoms look like a game logic problem, not an input problem.
Understanding the 128-Event Limit
Pygame’s internal event queue is a ring buffer with a default capacity of 128 events. Modern mice generate MOUSEMOTION events at 500 to 8000 times per second depending on the polling rate. Joysticks generate JOYAXISMOTION events for every tiny change in axis position. If your game runs at 60 FPS and processes events once per frame, the OS has 16 milliseconds to fill the queue between each drain. A high-polling-rate mouse can generate 130—plus events in that window alone, overflowing the queue and discarding everything that arrived after position 128 — including your KEYDOWN event.
You can verify overflow is happening by temporarily adding a queue size check:
import pygame
# Check queue size before draining (for debugging only)
queue_size = len(pygame.event.get(pump=False))
if queue_size >= 120: # approaching the limit
print(f"Warning: event queue at {queue_size}/128")
# Then drain normally
for event in pygame.event.get():
handle_event(event)
Note that pump=False peeks without processing — useful only for this diagnostic. In production code, always pass through the full drain without peeking first, since peeking adds a second queue read per frame.
The Fix: Always Call pygame.event.get() Every Frame
The fundamental fix is simple: call pygame.event.get() every frame, unconditionally. This returns all queued events as a list and simultaneously clears them from the queue. Even frames where you have no events to handle must still drain the queue, or it continues to fill.
The trap developers fall into is calling pygame.event.pump() instead on “quiet” frames. pump() processes the internal event loop and updates pygame’s internal state (like pygame.key.get_pressed()), but it does not clear the event queue. If you use pump() on frames where you don’t need events, the queue still fills:
# WRONG: pump() does not drain the event queue
pygame.event.pump() # queue keeps filling
# CORRECT: get() drains AND processes
events = pygame.event.get() # queue is now empty
for event in events:
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
handle_keydown(event.key)
If your game uses pygame.key.get_pressed() for held-key detection (which reads current keyboard state independently of the queue), you still need to call pygame.event.get() every frame for the queue to stay drained and for pygame.QUIT events to be received.
Blocking High-Frequency Events with set_blocked()
Even with diligent queue draining, a very high-polling-rate device can still briefly overflow the queue between two consecutive get() calls if your frame rate dips. The robust solution is to prevent high-frequency events from entering the queue at all, using pygame.event.set_blocked().
import pygame
pygame.init()
# Block events that generate hundreds of queue entries per second
pygame.event.set_blocked([
pygame.MOUSEMOTION, # hundreds/sec on high-DPI mice
pygame.JOYAXISMOTION, # fires continuously for any stick input
pygame.JOYBALLMOTION, # rare but noisy
pygame.JOYHATMOTION, # d-pad hat events
])
# Now the queue only receives events you actually care about
After blocking MOUSEMOTION, read mouse position with pygame.mouse.get_pos() in your game loop instead of processing motion events. After blocking JOYAXISMOTION, read axis values with pygame.joystick.Joystick(id).get_axis(axis) polled once per frame. Both approaches give you current state without queue pressure.
The Whitelist Approach: set_allowed()
The inverse of set_blocked() is set_allowed(), which first blocks all event types and then selectively allows only the ones you list. For games with a small, well-defined set of input events, this is the cleanest approach — the queue can only ever contain events you actually handle:
# Block everything first (pygame handles this internally)
pygame.event.set_blocked(None) # None means all event types
# Allow only what you need
pygame.event.set_allowed([
pygame.QUIT,
pygame.KEYDOWN,
pygame.KEYUP,
pygame.JOYBUTTONDOWN,
pygame.JOYBUTTONUP,
pygame.JOYDEVICEADDED,
pygame.JOYDEVICEREMOVED,
])
With this setup the queue is bounded by how quickly the player can press and release buttons, which is several orders of magnitude slower than mouse polling rates. Queue overflow becomes impossible in practice. The trade-off is that any event type you forget to whitelist is silently lost — so add VIDEORESIZE and ACTIVEEVENT if your game handles window resizing or focus changes.
Clearing the Queue on Scene Transitions
A separate but related problem: stale events from one game state being processed in the next. When a loading screen completes and your game transitions to the main menu, any button presses the player made during loading are still sitting in the queue. The player who impatiently pressed Enter five times while waiting may find their character has already jumped, confirmed a dialog, or triggered an action they didn’t intend.
Use pygame.event.clear() immediately before entering a new scene to discard all queued events:
def transition_to_scene(new_scene):
# Discard events accumulated during the previous scene
pygame.event.clear()
current_scene = new_scene
# Example: after a loading screen
load_assets()
transition_to_scene(MainMenuScene())
You can optionally pass a list of event types to clear() to discard only specific categories — for example, clearing only KEYDOWN and JOYBUTTONDOWN while preserving QUIT events so the player can still close the window during a transition.
Diagnosing Dropped Events in a Running Game
If you suspect events are being dropped but can’t reproduce it reliably, add temporary instrumentation that logs queue depth and event type distribution to a file. Combined with Bugnet’s crash and event reporting, you can correlate player-reported “inputs not responding” tickets with frame-time spikes or specific scene transitions in your session data:
import collections, time
event_log = collections.Counter()
frame_start = time.perf_counter()
events = pygame.event.get()
event_log.update(e.type for e in events)
# Log every 300 frames (~5 seconds at 60fps)
if frame_count % 300 == 0:
print(f"Event distribution: {dict(event_log)}")
event_log.clear()
High counts of MOUSEMOTION (in the thousands per 5-second window) confirm queue pressure. High counts of JOYAXISMOTION with a joystick connected confirm the joystick is the source. Both point directly to the set_blocked() fix.
“Pygame doesn’t tell you when the queue overflows — it just quietly drops events. The fix is always to drain the queue every frame and block the noisy event types you’re not using.”
Block MOUSEMOTION and JOYAXISMOTION on day one — you almost never need them in the queue and they’re the cause of 90% of event overflow reports.