A small thing in our multiplayer prototype was behaving like a haunting. When a player picked up an item, a little effect played and the HUD updated. Most of the time. Maybe one in twenty, on a busy frame, the effect simply did not play for other clients, even though the pickup itself definitely happened and the inventory was correct. No error, no log, no crash. Just an event that sometimes did not arrive.
The culprit was a single word in a UFUNCTION macro, and it is one of those Unreal footguns that everyone steps on exactly once.
The cosmetic event was a multicast RPC declared like this:
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayPickupFX(FVector Location);
Unreliable is doing exactly what it says. Unreliable RPCs are fire-and-forget over UDP. There is no acknowledgement and no resend. If the packet is dropped, or if the engine decides it is over its bandwidth budget for that connection this tick, the call is quietly discarded. That is the right choice for things that happen constantly and self-correct, like movement, where the next update is along in a few milliseconds and a missing one does not matter. It is the wrong choice for a one-shot event that has to land or be lost forever.
So why did it only fail under load? Because that is precisely when unreliable RPCs get culled. On a quiet frame everything fits and everything arrives, which is the cruellest possible behaviour for a bug: it works perfectly right up until the moment the game is busy, which is the only moment anyone is watching. We had tested it to death in calm conditions and shipped a coin flip.
The obvious fix is to flip it to Reliable. Reliable RPCs are acknowledged and resent until they arrive, so the event cannot simply evaporate:
UFUNCTION(NetMulticast, Reliable)
void MulticastPlayPickupFX(FVector Location);
That works, and for a genuine one-shot gameplay event it is correct. But Reliable is not free, and here is the second half of the footgun. The reliable channel is ordered and finite. If you spam reliable RPCs, you can saturate it, and a saturated reliable buffer will disconnect the client rather than silently drop, which is a far louder failure than the one you were trying to fix. So "make everything reliable" is not the lesson.
The actual lesson is to match the channel to the data. Continuous, self-correcting state stays unreliable and you let the occasional drop wash out. Discrete, must-happen events go reliable and you keep them rare. And for cosmetic-but-important things like our pickup effect, the cleanest answer was often not an RPC at all but a replicated property with a RepNotify, so the state replicates through the normal reliable property channel and the effect fires from the notify on every client that receives the change, with no per-event packet to lose.
We went with the replicated property in the end. The pickup state was already replicating for the inventory, so the effect could ride along on data we were sending anyway, and the explicit RPC went away entirely. Fewer packets, no haunting.
The thing I will remember is that Unreliable is not a performance hint you sprinkle on to be polite to the network. It is a correctness contract that says "I do not care if this is lost." Read it that way every time, and the question answers itself.