Quick answer: GameMaker automatically destroys instances on room change but does not free data structures, surfaces, or audio buffers. Add a Clean Up event to every object that creates a ds_map, ds_list, ds_grid, or surface, and call the corresponding destroy/free function there. Also audit persistent objects — they survive room changes and can accumulate resources invisibly.

Your game runs fine on the first playthrough. By the third time the player enters and exits a level, it’s visibly slower. By the tenth, the framerate has halved. Open Task Manager and you’ll see memory climbing steadily with every room transition. GameMaker is holding onto data you thought was gone — and finding it requires knowing exactly which resources GameMaker manages automatically and which ones you must free yourself.

The Symptom

The leak pattern is almost always the same: memory grows in discrete steps, each step corresponding to a room change. If you have a main menu room and a gameplay room and the player keeps pressing Escape to return to the menu and re-entering the level, memory climbs by roughly the same amount each cycle.

You can observe this at runtime by displaying fps_real on-screen. Unlike game_get_frames_per_second() (which returns the target frame rate), fps_real returns the actual measured frame rate. As memory pressure increases and garbage collection runs more frequently, fps_real will show periodic dips that get worse over time.

// Draw event — simple real-time memory pressure monitor
draw_set_colour(c_yellow);
draw_text(16, 16, "FPS: " + string(fps_real));
draw_text(16, 32, "Target: " + string(game_get_frames_per_second()));

If fps_real is trending downward session after session while game_get_frames_per_second() stays constant, you have a leak.

What Causes This

GameMaker manages instance memory automatically. When a room changes, all non-persistent instances are destroyed and their memory is reclaimed. But GameMaker has several resource types that live in a separate, manually-managed heap:

None of these are freed when an instance is destroyed. The handle becomes orphaned in memory. If you create a new data structure in the Create event every time an instance spawns, and never free it, you accumulate one orphaned structure per room visit.

The Fix

Use the Clean Up Event, Not Just Destroy

GameMaker has two relevant events for cleanup: Destroy and Clean Up. The Destroy event only fires when instance_destroy() is called explicitly. The Clean Up event fires whenever the instance is removed from memory — including room changes, explicit destruction, and game end. Always use Clean Up for freeing resources.

/// Create event
inventory     = ds_map_create();
item_list     = ds_list_create();
score_grid    = ds_grid_create(10, 10);
undo_stack    = ds_stack_create();

/// Clean Up event
if (ds_exists(inventory, ds_type_map))   ds_map_destroy(inventory);
if (ds_exists(item_list, ds_type_list))  ds_list_destroy(item_list);
if (ds_exists(score_grid, ds_type_grid))  ds_grid_destroy(score_grid);
if (ds_exists(undo_stack, ds_type_stack)) ds_stack_destroy(undo_stack);

The ds_exists() guard is important: if you ever double-call the Clean Up event (which can happen with persistent objects) or if the data structure was never successfully created, calling destroy on an invalid handle can crash or corrupt memory.

Free Surfaces Before the Room Changes

Surfaces are invalidated by GameMaker on room change (the GPU memory is reclaimed), but the surface handle remains in GML memory until you call surface_free(). The correct pattern:

/// Create event
render_surface = surface_create(room_width, room_height);

/// Clean Up event
if (surface_exists(render_surface))
{
    surface_free(render_surface);
}

/// Step event (recreate if invalidated mid-session)
if (!surface_exists(render_surface))
{
    render_surface = surface_create(room_width, room_height);
}

The Step event guard handles the case where the OS or the driver invalidates your surface (e.g., the window is minimized on some platforms). Without it, drawing to an invalidated surface silently fails. With it, you always have a valid surface to draw to.

Clean Up Audio Buffers

Dynamic audio created with audio_create_buffer_sound() is not freed automatically. This is less common than ds leaks but can be severe if your game streams procedural audio or loads sound effects from disk at runtime:

/// Create event (streaming audio example)
audio_buf   = buffer_create(65536, buffer_fixed, 1);
// ... fill buffer with PCM data ...
audio_sound = audio_create_buffer_sound(audio_buf,
    audio_mono, 44100, 0, 65536, audio_s16);

/// Clean Up event
if (audio_exists(audio_sound)) audio_free_buffer_sound(audio_sound);
if (buffer_exists(audio_buf))  buffer_delete(audio_buf);

Note that audio_free_buffer_sound() frees the sound asset but not the underlying buffer. You must call buffer_delete() separately.

The with(all) Destroy Pattern

If you have a room with many dynamically created instances and want to ensure everything is cleaned up before transitioning, you can use a controller object that calls cleanup on all instances before the room changes:

/// RoomController — Room End event
with (all)
{
    // Fire Clean Up logic on every instance
    // (Clean Up runs automatically, but this pattern
    // lets you order cleanup if needed)
    if (variable_instance_exists(id, "data_map"))
    {
        if (ds_exists(data_map, ds_type_map))
            ds_map_destroy(data_map);
    }
}

This is useful when instances have inconsistent cleanup behavior across a large codebase. A better long-term solution is to enforce the Clean Up event pattern on every object that creates heap resources.

Persistent Objects: The Hidden Accumulator

Persistent objects are the sneakiest source of room-change leaks. A persistent object survives room transitions, which is intentional — but if that object creates a new data structure in its Room Start event without destroying the previous one, it accumulates one structure per room visit.

/// BROKEN — persistent object, Room Start event
session_data = ds_map_create();  // creates a new map every room — old one leaked!

/// FIXED — persistent object, Room Start event
if (ds_exists(session_data, ds_type_map))
    ds_map_destroy(session_data);
session_data = ds_map_create();

Also watch for persistent objects that accumulate child instances. If a persistent manager object creates enemies or particles and stores their IDs in a list, check that the list entries are cleaned up when those instances are destroyed. A list of stale instance IDs doesn’t directly leak memory (instance IDs are just integers), but if anything iterates over the list and calls functions on those IDs, it will throw errors or undefined behavior when the IDs are reused.

You can audit active persistent instances at runtime:

// Count all instances of a persistent object to verify no duplicates
show_debug_message("GameManager count: " +
    string(instance_number(obj_GameManager)));

If this prints 2 or more, you have duplicate persistent instances accumulating — a classic sign that the object is also placed in the new room’s editor layout in addition to persisting from the previous room.

Monitoring Performance Degradation

Beyond fps_real, GameMaker’s built-in debugger (available in YYC and VM debug builds) includes a memory graph. Run a debug build, open the debugger, and watch the Memory panel as you cycle through rooms. A healthy game shows memory rising during room load and falling after the previous room’s resources are freed. A leaking game shows a staircase pattern where each room entry raises the floor.

For production monitoring, a lightweight approach is to write memory stats to a log file periodically:

/// obj_DebugMonitor — Step event (debug builds only)
if (fps_real < game_get_frames_per_second() * 0.9)
{
    show_debug_message(
        "[PERF WARNING] fps_real=" + string(fps_real) +
        " room=" + room_get_name(room)
    );
}

Related Issues

If you’ve freed all data structures but memory still climbs, check these additional sources:

The Clean Up event is your best friend — if every object that creates a heap resource has a matching Clean Up event that frees it, room-change leaks become essentially impossible.