Our Godot Game Only Crashed on Expensive PCs (Here's Why)

Published: (January 16, 2026 at 04:02 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

The Actual Problems

We tracked it down to four separate issues that were all making things worse together.

1. Loading Resources During Gameplay

This looks harmless:

func start_boss_fight():
    var boss_scene = load("res://scenes/InspectionBossFight.tscn")
    var boss = boss_scene.instantiate()

That load() call freezes everything for 100‑500 ms while it reads from disk. Combine that with shader compilation and you hit Windows TDR (Timeout Detection and Recovery). Windows kills any process that doesn’t respond to the GPU driver within ~2 seconds. No crash report, just dead.

Fix: Preload at startup instead.

const InspectionBossFightScene = preload("res://scenes/InspectionBossFight.tscn")

func start_boss_fight():
    var boss = InspectionBossFightScene.instantiate()  # instant

Do the same for shader materials: create templates at startup and duplicate them instead of creating them at runtime.


2. Awaits That Wait Forever

func _on_achievement_unlocked():
    await EventBus.game_saved
    show_notification()

If the save fails and never emits that signal, the coroutine waits forever and the game hangs.

Fix: Fire and forget.

func _on_achievement_unlocked():
    EventBus.request_save.emit()
    show_notification()  # don't wait for confirmation

3. Tweens Piling Up

Every UI animation created a new tween, and we never killed the old ones. After a few hours of gameplay, thousands of dead tween references sat in memory.

Fix: Track them and kill the old one before making a new one.

var _active_tweens: Dictionary = {}

func fade_ambient_light():
    if _active_tweens.has("ambient_fade") and _active_tweens["ambient_fade"].is_valid():
        _active_tweens["ambient_fade"].kill()

    var tween = create_tween()
    _active_tweens["ambient_fade"] = tween
    tween.tween_property(light, "energy", 0.5, 1.0)

4. Loops With No Safety Limits

Our fighter‑cleanup loop could churn through thousands of invalid entries in a single frame:

func cleanup_fighters():
    for i in range(fighters.size() - 1, -1, -1):
        if not is_instance_valid(fighters[i]):
            fighters.remove_at(i)

Fix: Add iteration limits.

const MAX_ITERATIONS_PER_FRAME = 100

func cleanup_fighters():
    var iterations = 0
    for i in range(fighters.size() - 1, -1, -1):
        iterations += 1
        if iterations > MAX_ITERATIONS_PER_FRAME:
            break
        if not is_instance_valid(fighters[i]):
            fighters.remove_at(i)

For larger operations (e.g., applying buffs to 100+ units at once), chunk the work across frames:

func apply_cascade_inspiration(units: Array):
    var index = 0
    while index = units.size():
                break
            units[index].apply_inspiration()
            index += 1
        await get_tree().process_frame

TL;DR

  • Preload resources at startup. Runtime load() on Windows can trigger GPU driver timeouts.
  • Don’t await signals that might never fire. Use fire‑and‑forget patterns when appropriate.
  • Track your tweens. Kill old ones before creating new ones.
  • Put limits on loops. Especially those processing dynamic arrays.
  • Chunk big operations across frames. This keeps the main thread responsive.

The kicker: high‑end PCs hit these bugs faster because they run more game loops per second. Sometimes the best hardware finds the worst problems.

We’re Lost Rabbit Digital on GitHub. Starbrew Station is built with Godot 4.5.

Back to Blog

Related posts

Read more »

Python: Tprof, a Targeting Profiler

Python: introducing tprof, a targeting profiler Profilers measure the performance of a whole program to identify where most of the time is spent. But once you’...