It’s been fixed on 5.4.
If you’re on 5.3 like I am, and can’t build a custom engine version, luckily you can apply their intermediate fix since AbilitySystemComponent::RemoveGameplayCue_Internal is virtual:
// Copied from UE 5.4 to fix Cues not being removed on server
// git deb931486115a3ab79e319db81821366dfdb969c
void ULyraAbilitySystemComponent::RemoveGameplayCue_Internal(const FGameplayTag GameplayCueTag, FActiveGameplayCueContainer& GameplayCueContainer)
{
if (IsOwnerActorAuthoritative())
{
int32 NumMatchingCues = 0;
for (const FActiveGameplayCue& GameplayCue : GameplayCueContainer.GameplayCues)
{
NumMatchingCues += (GameplayCue.GameplayCueTag == GameplayCueTag);
}
if (NumMatchingCues > 0)
{
// AbilitySystem.GameplayCueNotifyTagCheckOnRemove assumes the tag is removed before any invocation of EGameplayCueEvent::Removed.
// We cannot use GameplayCueContainer.RemoveCue because that removes the cues while updating the TagMap.
// Instead, we need to manually count the removals, update the tag map, then Invoke the Cue events while removing the Cues.
UpdateTagMap(GameplayCueTag, -NumMatchingCues);
for (int32 Index = GameplayCueContainer.GameplayCues.Num() - 1; Index >= 0; --Index)
{
const FActiveGameplayCue& GameplayCue = GameplayCueContainer.GameplayCues[Index];
if (GameplayCue.GameplayCueTag == GameplayCueTag)
{
// Call on server here, clients get it from repnotify on the GameplayCueContainer rather than a multicast rpc
InvokeGameplayCueEvent(GameplayCueTag, EGameplayCueEvent::Removed, GameplayCue.Parameters);
GameplayCueContainer.GameplayCues.RemoveAt(Index);
}
}
// Ensure that the clients are aware of these changes ASAP
GameplayCueContainer.MarkArrayDirty();
ForceReplication();
}
}
else if (ScopedPredictionKey.IsLocalClientKey())
{
GameplayCueContainer.PredictiveRemove(GameplayCueTag);
}
}
The final fix is 45e395166cdb14ef49088b945e61fe3196efbd9c.