Quick answer: Connect signals in _EnterTree/_Ready and disconnect in _ExitTree. Use IsInstanceValid to guard handlers that may run after the receiver was freed. Or pass ConnectFlags.OneShot for one-time signals that auto-disconnect.

Here is how to fix Godot 4 C# signal connections that produce errors after scene unload, leak references, or fire on already-destroyed objects. C# garbage collection lifetime is independent of Godot’s C++ node tree. The fix is symmetric connect/disconnect plus validity checks.

The Symptom

Connecting a signal works. After scene change or QueueFree, signals fire and you get Cannot resolve method on freed instance errors. Or memory grows over time as connections never disconnect.

What Causes This

C# delegate holds Godot reference. A C# delegate keeps a managed reference to the receiver. After the Node is freed by Godot, the delegate is still in the signal’s connection list.

Asymmetric lifetime. Connect in Ready, never disconnect, scene unloads, signal fires → error.

Lambda captures. Lambdas capturing this hold the receiver alive longer than expected.

The Fix

Step 1: Connect in EnterTree, disconnect in ExitTree.

using Godot;

public partial class Player : Node2D
{
    public override void _EnterTree()
    {
        GameState.Instance.ScoreChanged += OnScoreChanged;
    }

    public override void _ExitTree()
    {
        if (IsInstanceValid(GameState.Instance))
            GameState.Instance.ScoreChanged -= OnScoreChanged;
    }

    private void OnScoreChanged(int score) { GD.Print(score); }
}

Step 2: For one-time events, use ConnectFlags.OneShot.

tween.Finished += OnFinished;   // stays connected

// or one-shot via Connect API
tween.Connect(Tween.SignalName.Finished,
    Callable.From(OnFinished),
    (uint)ConnectFlags.OneShot);

OneShot auto-disconnects after firing once.

Step 3: Guard handlers with IsInstanceValid.

private void OnScoreChanged(int score)
{
    if (!IsInstanceValid(this)) return;   // receiver freed
    // safe to use Node members
}

Step 4: Avoid lambda captures.

// Avoid - lambda holds 'this'
GameState.Instance.ScoreChanged += s => { Label.Text = s.ToString(); };

// Prefer - named method, easy to disconnect
GameState.Instance.ScoreChanged += OnScoreChanged;

Step 5: Use weak signals for autoload connections. Autoloads outlive scenes. Connecting from a scene’s node to an autoload can leak. The OneShot or ExitTree disconnect ensures clean teardown.

“Symmetric connect and disconnect. IsInstanceValid for safety. OneShot for one-time. Memory and signals stay clean.”

Related Issues

For GDScript signal disconnects, see Signal Lost After Scene Reload. For C# export build, see C# Export Build.

EnterTree connect, ExitTree disconnect. IsInstanceValid in handlers. No leaks.