Quick answer: If you subscribe only to performed on an InputAction that has a Tap or Hold interaction attached, the canceled callback will fire at an unexpected moment or not at all because the interaction resolves the phase itself. Subscribe to all three callbacks and remove or replace the interaction to see the true button release.

Here is how to fix Unity InputAction callbacks that swallow the Ended phase. You wire up action.performed and action.canceled, press and release the button, and the canceled callback either fires at the wrong moment or never fires at all. The button physically came back up, but the Input System appears to disagree.

The Symptom

You have a jump or interact action defined in the Input Actions asset. In code, you subscribe to both performed and canceled so you can know exactly when the button is pressed and when it is released. On paper this should be symmetrical: press triggers performed, release triggers canceled. In practice, you see one of three broken behaviors.

Sometimes canceled fires the instant you press the button, long before you release it. Sometimes canceled fires the moment you release, but performed never fired at all, so you get a release with no matching press. And sometimes performed fires correctly but canceled is simply silent — the action seems to end itself internally without notifying you.

This becomes obvious in any system that needs to track a button held down: charged attacks, grapple hooks, aim toggles, or any hold-to-continue mechanic. The symptom is that the held state gets stuck true or stuck false, and it will not clear on release.

What Causes This

The Unity Input System phases — Waiting, Started, Performed, Canceled, and Disabled — are not a simple press/release mapping. They are driven by interactions. An interaction is a small state machine that decides when an action has been satisfied. Examples include Press, Hold, Tap, SlowTap, and MultiTap. If you attach an interaction to a binding, that interaction controls phase transitions, not the raw control state.

A Tap interaction fires performed only if the button is released within a short window (default 0.2s). If you hold the button longer than the tap threshold, it fires canceled instead, never reaching performed. A Hold interaction does the opposite — it requires you to hold for a threshold, only then firing performed. When you release after performed has fired, canceled fires immediately. But if you release before the hold threshold, canceled fires without performed ever running.

The default interaction for a Button-type action, when you do not add an explicit one, is Press. Press fires performed on button down and canceled on button up — which matches most people’s mental model. The moment someone adds a Hold or Tap to make the action feel better, the phase timing shifts and callbacks stop aligning with the physical button.

There is a second cause that looks identical: the action map is not enabled in the frame you expect. If you call actions.Disable() in a scene unload or in a state machine transition, any action that was mid-phase gets forced to canceled without warning. The next press in a new scene will fire started and performed, but your release handler may have already fired in the previous scene.

The Fix

Step 1: Subscribe to all three callbacks and log the phase. This single diagnostic step usually reveals the problem in seconds.

using UnityEngine.InputSystem;

public class InputDebug : MonoBehaviour
{
  public InputActionReference jumpAction;

  void OnEnable()
  {
    jumpAction.action.started   += ctx => Log("started", ctx);
    jumpAction.action.performed += ctx => Log("performed", ctx);
    jumpAction.action.canceled  += ctx => Log("canceled", ctx);
    jumpAction.action.Enable();
  }

  void Log(string label, InputAction.CallbackContext ctx)
  {
    Debug.Log($"{label}: phase={ctx.phase} interaction={ctx.interaction?.GetType().Name}");
  }
}

Press and release the button a few times. If you see canceled without a preceding performed, you have a Tap interaction that did not resolve. If you see performed followed by nothing on release, you have a stale interaction or a disabled action map.

Step 2: Review interactions on the binding. Open the Input Actions asset, select the action, then select the binding under it. In the right-hand inspector, expand the Interactions list. If you see Hold, Tap, SlowTap, or MultiTap attached here, the default Press behavior is overridden. Remove interactions you do not need, or change them to Press if you want symmetrical performed/canceled on the physical button.

If you need both a hold and a tap behavior from the same button, do not stack interactions on the same binding. Instead create two actions — one with Tap, one with Hold — and subscribe to each separately. Stacking interactions creates phase races that are almost impossible to reason about.

Step 3: For Hold-based actions, track the held state yourself. If you cannot remove the Hold interaction (for example, you want a minimum hold before jump triggers), track the button down state independently using the WasPressedThisFrame and WasReleasedThisFrame helpers on the control, not the action phase.

void Update()
{
  var button = jumpAction.action.activeControl as ButtonControl;
  if (button == null) return;

  if (button.wasPressedThisFrame)   isHeld = true;
  if (button.wasReleasedThisFrame) isHeld = false;
}

Step 4: Confirm the action map is enabled. In OnEnable call actions.Enable(), and in OnDisable call actions.Disable(). Do not toggle maps mid-frame based on gameplay state unless you are prepared to handle the synthetic canceled that Unity emits when disabling an in-progress action.

If your action map is disabled during an active phase, the Input System synthesizes a canceled event with phase Disabled. Log ctx.phase to distinguish a real release from a forced cancel.

Why This Works

The Input System is a phase-driven state machine, not an event router over raw button states. Interactions are the rules that decide which phase the action is in at any given moment. When you understand that performed and canceled are outputs of an interaction — not direct translations of button up and button down — the missing Ended phase stops being mysterious. You are reading the interaction’s verdict, not the hardware.

Using WasPressedThisFrame on the underlying control lets you bypass the interaction entirely when you need raw button timing. And keeping action maps enabled precisely around the lifetime of the component that cares about them eliminates the synthetic cancels that happen during map toggling.

Related Issues

If your controller bindings are not being detected at all, see Fix: Unity Input System Controller Bindings Not Working — the action map may be scheme-filtered to keyboard only.

For input that appears to fire twice on a single press, see Fix: Unity InputAction Firing Twice Per Press. Double callbacks often come from subscribing in both OnEnable and Awake.

Log all three phases. The phase timeline never lies; the interaction does.