Quick Tip Evergreen

A dispatcher broadcast from inside a handler does not see later bindings

A Blueprint Event Dispatcher calls its bindings in registration order unless removals have reordered the list: ProcessMulticastDelegate iterates a copy from front to back. When one handler broadcasts another dispatcher, only bindings already registered with that dispatcher are included. Bindings added by later handlers do not exist yet. In this example, registration order follows from the lifecycle phases in which the Bind Event to... nodes run across different Blueprints.

Table of contents

The dispatcher OnDelegate_1 has two bindings. The first handler broadcasts OnDelegate_2 while it runs. The second handler binds CustomEvent_3 to OnDelegate_2, but by then the broadcast is already over.

The cause is not a race condition. A dispatcher broadcast is a synchronous loop over an array: one thread, one frame, the same result every time. The call order of the bindings is deterministic, and relying on it is legitimate, provided you know where that order comes from and what changes it. It comes from the lifecycle phase in which each Bind Event to… node ran.

The test setup

UE 5.7, pure Blueprint, two separate Blueprints. BP_GameInstance_Test owns both dispatchers and CustomEvent_1, which is bound in Init:

BP_GameInstance_Test: Init binds CustomEvent_1 to OnDelegate_1. CustomEvent_1 itself logs [1] and then broadcasts OnDelegate_2.

The actor BP_DispatcherTest binds to the same OnDelegate_1 in BeginPlay, and its handler is what creates the binding on OnDelegate_2:

BP_DispatcherTest: BeginPlay binds CustomEvent_2 to OnDelegate_1. CustomEvent_2 logs [2] and only then binds CustomEvent_3 (log [3]) to OnDelegate_2.

A key press in the Level Blueprint fires the broadcast. After calling OnDelegate_1, the graph logs [END TEST]. That marker marks the end of the synchronous execution triggered by the key press, and it makes the log comparisons further down readable:

LVL_DispatcherTest: the key press broadcasts OnDelegate_1 on the GameInstance, then logs [END TEST].

One key press, and the Output Log shows:

LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [END TEST]

[3] never appears. A breakpoint on CustomEvent_3 is never hit. There is no compile error, no red node, and no warning. From the engine’s point of view nothing went wrong: the dispatcher called everything that was registered at the moment of the call, and CustomEvent_3 was not registered then.

This is not a race condition

The symptom suggests a race condition: the binding and the broadcast are competing, and the broadcast wins. That explanation is false and sends the diagnosis in the wrong direction.

Nothing here is racing anything. When CustomEvent_1 broadcasts OnDelegate_2, CustomEvent_2 has not started running yet. Not because it was too slow, but because it comes later in program order. Run the same project a hundred times and it behaves identically a hundred times.

The distinction has practical consequences. A race condition is something you look for in timing and try to fix with a delay added just in case. A delay does work here, but not for the reason that hypothesis suggests: it doesn’t give another thread time to catch up, it moves the OnDelegate_2 broadcast past the end of the entire OnDelegate_1 handler list. It is worth knowing which of the two you are buying.

Proving the order is deterministic

The setup above shows where the invisible order comes from, but it mixes two variables at once. One Blueprint is enough to isolate it: all three custom events on BP_GameInstance_Test, both bindings to OnDelegate_1 on a single exec wire in Init, no actor and no BeginPlay.

Arrangement one: in Init, CustomEvent_1 is bound first, then CustomEvent_2.
LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [END TEST]

Now the only change in the entire project: the exec wire between the two Bind Event to OnDelegate_1 nodes is swapped. The nodes, the events, their contents, and the Level Blueprint stay untouched.

Arrangement two: identical contents, the two Bind Event nodes in reverse order.
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [3]
LogBlueprintUserMessages: [END TEST]

CustomEvent_2 ran first, bound CustomEvent_3 to OnDelegate_2, and only then did CustomEvent_1 broadcast that dispatcher. Reordering two nodes on an exec wire changed the result predictably and repeatably. The result follows directly from the order of execution: in one arrangement, registration happens after the broadcast; in the other, it happens before it.

Warning
When you repeat this test, press the key once per run and restart PIE between arrangements. A second press in arrangement one does log [3], because the binding that CustomEvent_2 created during the first broadcast is still there. That looks like instability, but it is leftover state from the previous call.

Where the order comes from

A Blueprint dispatcher is an FMulticastScriptDelegate, and its broadcast lives in ScriptDelegates.h:917 (5.7):

if( InvocationList.Num() > 0 )
{
    // Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast
    typedef TArray< UnicastDelegateType, TInlineAllocator< 4 > > FInlineInvocationList;
    FInlineInvocationList InvocationListCopy = FInlineInvocationList(InvocationList);

    // Invoke each bound function
    for( typename FInlineInvocationList::TConstIterator FunctionIt( InvocationListCopy ); FunctionIt; ++FunctionIt )
    {
        if( FunctionIt->IsBound() )
        {
            FunctionIt->template ProcessDelegate<UObjectTemplate>(Parameters);
        }
    }
}

TConstIterator runs front to back, and new bindings are appended to the end of the array. Hence the rule: in a Blueprint dispatcher, first bound is first called.

That leaves the question of why CustomEvent_1 runs first. Not the graph: neither of the two Blueprints contains a node that sets the call order. The lifecycle decides. UGameInstance::Init (Private/GameInstance.cpp:129) runs before the map is loaded, and therefore before BeginPlay on any actor in the world. The world startup timeline is laid out separately in Subsystem and manager actor lifecycles.

Insight
In the original two-Blueprint setup, lifecycle phases determine the handler order: the binding in Init runs before the binding in BeginPlay. To trace the order in your own project, find every Bind Event to… node targeting that dispatcher and establish when each one executes, including the execution order within a single phase. That information may be spread across several graphs.

Same name, opposite order

The C++ equivalent iterates the other way, with a comment that says why (MulticastDelegateBase.h:292-293):

// call bound functions in reverse order, so we ignore any instances that may be added by callees
for (int32 InvocationListIndex = LocalInvocationList.Num() - 1; InvocationListIndex >= 0; --InvocationListIndex)
Call orderBinding added during the broadcast
Blueprint dispatcher (FMulticastScriptDelegate)registration order — first bound is called firstskipped; the list is copied before the loop
C++ delegate (TMulticastDelegate)reverse of registration — last bound is called firstskipped; the loop runs backwards

So one name, “multicast delegate,” covers two implementations with opposite iteration order. The order a graph relies on is a property of the specific implementation, not of the concept.

Same symptom, different cause

The comment above the copy, “just in case the list is modified by one of the callbacks during the broadcast,” describes a different case with an identical symptom. The copy means that a binding added to a dispatcher that is currently broadcasting will not be called in that broadcast. If CustomEvent_2 bound CustomEvent_3 to OnDelegate_1, the same dispatcher that is calling it right now, CustomEvent_3 would stay silent as well. The cause would be the frozen copy taken before the first handler, not the registration order.

Note
Both traps belong to the same family: the state of the binding list at the moment of the call is the only thing that counts. Anything bound after that does not exist for that call.

What the order does not guarantee

Moving a binding earlier in the lifecycle fixes the symptom, and it is a legitimate move. It is worth knowing what it assumes.

Epic does not promise this order. The multicast class documentation says so directly (DelegateSignatureImpl.inl:1024-1025):

Multicast delegates offer no guarantees for the calling order of bound functions. As bindings get added and removed over time, the calling order may change.

Removal from the list does not preserve order. RemoveInternal goes through RemoveAtSwap (ScriptDelegates.h:1069), so the last entry jumps into the freed index. The header warns about this on every removal method (ScriptDelegates.h:731, 771, 1045, 1056):

* Removes a function from this multi-cast delegate's invocation list (performance is O(N)).  Note that the
* order of the delegates may not be preserved!

The practical takeaway: the order is stable as long as nothing unbinds from the dispatcher. Unbinding from this dispatcher anywhere in the project can reorder its remaining listeners.

Note
Binding again does not move the event to the end of the list. The Bind Event to… node compiles to EX_AddMulticastDelegate (ScriptCore.cpp:3342), which calls FMulticastInlineDelegateProperty::AddDelegate (PropertyMulticastDelegate.cpp:497), which calls InvocationList.AddUnique (ScriptDelegates.h:1040). AddUnique leaves an existing entry at its current position, so binding a second time does nothing.
Warning
Relying on the order is fine as long as you can answer two questions: what sets it and what can change it. If the answer to both is “I do not know,” the graph works by accident.

What to do about it

One pattern forces a decision: a dispatcher handler that broadcasts another dispatcher while it runs. It opens a window in which some listeners have not registered yet. Five ways out, each with its own condition. The first three change the graph and have results shown below. The last two are a project convention rather than a change in the nodes.

Defer the broadcast by one tick

Instead of broadcasting OnDelegate_2 straight from CustomEvent_1, route it through Set Timer for Next Tick by Event and a separate BroadcastDelegate2 event. The OnDelegate_1 broadcast then runs through every handler, each one gets to bind, and OnDelegate_2 starts with a complete list.

CustomEvent_1 does not broadcast OnDelegate_2 directly. It hands that off to BroadcastDelegate2 through Set Timer for Next Tick by Event.
LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [END TEST]
LogBlueprintUserMessages: [3]

[3] landing after the [END TEST] marker shows exactly what this solution does: the broadcast moved out of the synchronous execution triggered by the key press and into the next frame.

This applies when no OnDelegate_2 listener binds later than the next frame. An actor spawned two frames later, or a widget created on demand, still misses out. It widens the window; it does not remove the dependency.

Tip
Set Timer for Next Tick by Event takes no time input, so it cannot accidentally be set to zero. Set Timer by Event can, and there Time = 0.0 clears the timer instead of firing it immediately.

Move the binding to a dispatcher that is guaranteed to fire later

CustomEvent_3 goes on OnDelegate_3, which is known to fire later. Here the same key press broadcasts it, right after OnDelegate_1.

CustomEvent_2 binds CustomEvent_3 to OnDelegate_3 instead of OnDelegate_2.
LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [3]
LogBlueprintUserMessages: [END TEST]

Cheap and effective, but it still depends on ordering: the order of separate broadcasts rather than the order of handlers within one broadcast. The condition is that nobody moves that call.

Turn the event into state

A dispatcher carries information only at the moment of the call. Whoever arrives late never gets it. A bDelegate2Broadcast bool on the sender side changes that rule: CustomEvent_1 sets it before the broadcast, and CustomEvent_2, once its binding is in place, checks it with a Branch and calls CustomEvent_3 directly if needed.

CustomEvent_1 sets bDelegate2Broadcast before the call. After binding, CustomEvent_2 checks the flag with a Branch and makes up for the missed broadcast.
LogBlueprintUserMessages: [1]
LogBlueprintUserMessages: [2]
LogBlueprintUserMessages: [3]
LogBlueprintUserMessages: [END TEST]

[3] appears synchronously here, still within the same key press. The late listener does not wait for another broadcast. It reads the state and catches up immediately. This is the only option that survives a listener created after everything else, at the cost of maintaining extra state.

Declare the order instead of relying on it silently

If the order is meant to be a contract, write it down: a comment on both Bind Event to… nodes stating which one comes first and what depends on it, an event name that identifies the phase, and an explicit condition that nothing unbinds from this dispatcher. This option documents the existing ordering dependency without changing the graph.

Separate the phases: bindings in one, broadcasts in the next

If every Bind Event to… runs in a phase earlier than any call, the order within the list stops meaning anything. Here the dependency disappears instead of moving further out. The condition is that the phase boundary is clearly defined in the project and nobody binds after it.

When this matters

As long as every binding to a given dispatcher lives in one Blueprint, the problem barely exists. The order is visible at a glance, and nobody changes it by accident.

The risk shows up once listeners of the same dispatcher spread across different lifecycle phases: GameInstance, a subsystem, GameMode, an actor placed in the level, a spawned actor, a widget created on demand. Call order stops being a property of the graph and becomes a property of the whole project: it is set by the phases in which all the Bind Event to… nodes attached to that dispatcher run. Move one of them to a different phase and the rest shift position in the list.

See also

Related pages