Verse Wiki — the Verse handbook for Blueprint authors
Chapter 7 · Lesson 24

Concurrency: spawn / race / sync — Command Your Clone Army

This is the lesson with no Blueprint counterpart: Blueprints have no real concurrency primitives, so "whoever finishes first wins" and "continue once all are done" are hand-rolled from flag variables and counters. Verse turns them into language keywords — spawn forks a clone, race cancels the losers, sync waits for everyone.

1. spawn: Send Out a Clone — the Main Line Doesn't Wait

Let's place this lesson properly first: Blueprints have no real concurrency primitives. When you make things happen "at the same time" in Blueprints, you split the logic across several Event Dispatchers, several timers, several Custom Events, each running on its own, and then reconcile their progress through variables — doable, but assembled entirely by hand. Verse builds this into the language: five concurrency expressions, keywords just like Branch. The first is spawn — send a clone off to do the job while the main line keeps walking, without so much as a glance back.

firework_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

firework_device := class(creative_device):

    LaunchFirework()<suspends>:void =
        Sleep(2.0)
        Print("Fireworks away!")

    OnBegin<override>()<suspends>:void =
        Print("Lighting the fuse...")
        spawn{ LaunchFirework() }
        Print("Main line isn't waiting for fireworks - moving on to set up the venue")

Read this graph first: firework_device is a Blueprint device with creative_device as its parent class. Inside, LaunchFirework is a custom function (its name carries the little clock icon — because it holds nodes that wait), and its body is "Delay 2 seconds → Print String 'Fireworks away!'". OnBegin (think Event BeginPlay) does three things: Print String 'Lighting the fuse'; use spawn to fork off a new wire that runs LaunchFirework without the main line waiting for it; then immediately Print String 'Main line isn't waiting for fireworks'.

Read the output order — one detail deserves a tap on the chalkboard: the new wire that spawn forks off first runs in one go until it hits its first "wait a moment" node (here, the Delay that is Sleep(2.0)), and only then hands execution back to the main line. So the log reads "Lighting the fuse" → "Main line isn't waiting for fireworks", and only two seconds later does "Fireworks away!" show up.

spawn comes with two iron rules. First, the braces may hold exactly one call to a function "with the little clock" — want to do three things at once? Pack all three into one <suspends> function first (that marker declares "there are waiting nodes in here"), then spawn that function. Second, it is the only one of the five concurrency expressions that can live on a "wire that can't wait": regular functions and bound-event responses alike. Next lesson you'll see its highlight moment — starting a stretch of waiting logic inside the response you bind to a device event (Subscribe) is all spawn.

One more thing to lock in now: a clone sent out by spawn is a horse let off the reins — even when the function that launched it ends, the clone isn't cancelled; it keeps running until its own work is done. Also, the spawn action actually pushes out an output: a receipt called a task, which you can later hand to Await() (wait for it to finish and collect the result) — the extra page has the full breakdown.

2. race: First Across the Line Wins — Losers Get Cancelled

race is an elimination match: every wire listed under race is a contestant, and they all start at once; the instant the first one crosses the finish line, the match is over — every other contestant is cancelled. race also carries an output pin, and what it emits is the champion's result:

turtle_rabbit_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

turtle_rabbit_device := class(creative_device):

    Turtle()<suspends>:string =
        Sleep(3.0)
        "Turtle"

    Rabbit()<suspends>:string =
        Sleep(1.0)
        "Rabbit"

    OnBegin<override>()<suspends>:void =
        Winner := race:
            Turtle()
            Rabbit()
        Print("The champion is {Winner}!")

Reading the graph: Turtle and Rabbit are two custom functions; each Delays for a while (3 s / 1 s) and then sends a string ("Turtle" / "Rabbit") to its own output pin. In OnBegin a race is laid down with those two wires hanging off it; when the race finishes, the winning wire's output is hooked into a fresh variable Winner (:= means "create a new variable and hook this value up to it"), then Print String announces the champion.

The rabbit sleeps 1 second, the turtle 3, so the race is decided at the 1-second mark: Winner gets "Rabbit". At that moment the turtle is still stuck on the Delay that is Sleep(3.0) — and that kind of "wait a moment" node is exactly where the cancellation happens: a loser isn't ripped out from the middle of a node; it waits until it reaches its next Sleep / Await (a waiting node) and only then exits quietly. This rule — cancellation only happens at waiting nodes — matters a lot, and the extra page "Cancellation and defer" runs an experiment so you can watch it with your own eyes.

Two house rules: race needs at least two wires under it, and it can only live on a "wire that can wait" (inside a function body marked <suspends>, or inside another concurrency block). Also, because race pushes a value out, the types the arms send must line up — one arm sending a string while another sends an integer turns Compile red on the spot. Want several nodes in one arm? Pack them into one wire with block: — that's exactly how the timeout recipe in section 4 is written.

3. sync: Everyone Arrives First; branch and rush: One Line Each

sync is race's mirror image: all the wires listed under it start together, and sync only lets you pass once every one of them has finished. Perfect for "nothing moves to the next scene until all of these are done" moments:

banquet_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

banquet_device := class(creative_device):

    CookMainDish()<suspends>:void =
        Sleep(3.0)
        Print("Main course is served")

    BakeDessert()<suspends>:void =
        Sleep(2.0)
        Print("Dessert is out of the oven")

    OnBegin<override>()<suspends>:void =
        sync:
            CookMainDish()
            BakeDessert()
        Print("Everyone's here - let the banquet begin!")

Reading the graph: CookMainDish and BakeDessert are two custom functions that Delay 3 s / 2 s and then each Print String one line. In OnBegin a sync is laid down with those two wires attached; only after both have run to the end does the Print String after the sync — "Everyone's here - let the banquet begin!" — get to execute.

The main course takes 3 seconds and the dessert 2 — but they're made in parallel, so "let the banquet begin" prints at the 3-second mark, not at 3 + 2 = 5. That is the whole point of concurrency: total time is set by the slowest member, not the sum of everyone. Like race, sync needs at least two wires under it and can only live on a "wire that can wait"; it too carries an output — a tuple bundling each wire's result, in the same top-to-bottom order you laid them out.

The remaining two members get one line each. branch: is spawn's "act now, report later" cousin — it forks off a new waiting wire and the main line continues immediately; but that wire's life is leashed to the surrounding graph: the moment that stretch of graph ends, the wire is revoked (think of it as "spawn on a leash"). rush: is "race without eliminations" — the moment the first contestant finishes, rush moves on with that result, but the others are not cancelled; they keep running until the surrounding graph ends and only then get cleared out.

Expression Waits for whom? Fate of the other tasks Usable on a "wire that can't wait"? Value of the expression
race The first to finish Cancelled at their next waiting node No The winner's result
sync All of them — (they all run to completion) No A tuple of all results
rush The first to finish Keep running; cancelled only when the surrounding graph ends No The earliest finisher's result
branch Doesn't wait Revoked when the surrounding graph exits No — (its value isn't used)
spawn Doesn't wait Lives independently; not cancelled even when that stretch of graph ends Yes (the only one) A task object

The "usable on a wire that can't wait?" column exposes the official design philosophy: structured concurrency. Wires forked by sync / race / rush / branch all have their lifespans managed by the stretch of graph they live in — when that graph wraps up, those wires pack up with it, and not one goes missing; only spawn's wires can "escape" that graph and live on independently. Hence the refreshingly blunt official advice: if one of the four structured siblings will do, don't use spawn; call on spawn only when you're stuck on a "wire that can't wait" (say, a bound-event response) with no other choice.

4. The Classic Recipe: race + Sleep = Timeout

Put race together with last lesson's Sleep and you get the most classic concurrency recipe in Verse — the timeout. The brief: the player must press a button within 10 seconds; press in time and the challenge succeeds, miss it and it's a timeout. Done any other way, you'd need a Timer, a switch variable, and manual state cleanup — no skipping any of the three; in Verse it's just a two-runner race: one wire waits for the button press, the other Delays 10 seconds — whoever finishes first wins, the loser is auto-cancelled, and you don't wire up a single cleanup node. Hit "Run next step" to watch this race get decided.

timeout_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

timeout_device := class(creative_device):

    @editable
    Button:button_device = button_device{}

    OnBegin<override>()<suspends>:void =
        Print("Press the button within 10 seconds - go!")
        race:
            block:
                Button.InteractedWithEvent.Await()
                Print("Pressed in time - challenge complete!")
            block:
                Sleep(10.0)
                Print("Time's up - the chance slipped away...")
        Print("race is over - the loser has left the stage")
Output log

Hit "Run next step" to watch the code execute line by line.

Note that both arms open with block: — block's job is to bundle several nodes into a single wire. Here neither wire emits a value at the end (void), so the types trivially line up; if you want the race itself to tell you "success or timeout", have each wire send out a logic (a Boolean true/false) — the race's output is your answer.

Your turn. The "curtain-call console" below has two keywords carved out: one is the expression that can fork a new wire from a "wire that can't wait" (like a bound-event response), the other is the one that waits for everyone to finish. Fill them back in, then hit "Check answer".

Reading the graph: in the top half, Fireworks and Music are two custom functions each carrying a Delay. OnEncore is a response "bound to a button — runs on each press" — it's a wire that can't wait, so to launch another round of fireworks inside it, your only move is to fork a new wire (first blank). In the bottom half, OnBegin needs "fireworks and music both finished before the curtain call" — that takes the block that waits for everyone (second blank).

finale_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

finale_device := class(creative_device):

    Fireworks()<suspends>:void =
        Sleep(2.0)
        Print("Fireworks burst together")

    Music()<suspends>:void =
        Sleep(3.0)
        Print("Music fades out")

    # This callback gets Subscribed to some button (details next lesson)
    OnEncore(Agent:agent):void =
        # A callback is not an async context - to fire another round, send out a clone
        ____{ Fireworks() }

    OnBegin<override>()<suspends>:void =
        # The curtain call only counts once fireworks and music are both done
        ____:
            Fireworks()
            Music()
        Print("Curtain call complete")

5. Common Pitfalls: the Concurrency Rookie Minefield

When a concurrency expression turns Compile red, the error tends to look ferocious, but the root causes are all tiny. Step on these mines in advance and you'll save yourself hours of debugging:

Blueprint Cross-Reference

This lesson's table looks different from the others: the left column is mostly not "some node" but "a workaround you hand-wrote." Blueprints have no concurrency primitives — which is exactly what makes this chapter the most valuable trade in the whole book.

How you do it in Blueprints How you write it in Verse Difference
Split the logic across a few Event Dispatchers / Custom Events, each fired separately and running on its own spawn{ MyAsyncFn() } In Blueprints "at the same time" is a side effect of scattered triggers — no single place says "a wire forks here"; spawn puts it on one line, so who forked what is plain to read
"Whoever finishes first wins": run two wires, hand-write a bool flag, first one to arrive sets it, the late one checks the flag with a Branch and bows out race: with two wires under it race carries "first across the line, everyone else cancelled" as built-in meaning. The flag version makes you guard re-entry and write the exit branch yourself; race is two indented lines
"Continue once all are done": add a counter variable, bump it as each wire finishes, check it against N sync: with several wires under it sync turns the counter into a language construct, and hands you every wire's result packed into a tuple; the counter version still needs somewhere to stash those results
The Sequence node Just keep writing down the indentation, or pack a wire with block: Sequence is not concurrency. It runs its exec pins one after another, Then 0 → Then 1. For genuine parallelism, Verse's answer is sync
Set Timer by Event to start background logic, then remember to Clear Timer on the way out — forget and you get ghosts branch: A branch wire is tied to the scope it lives in and retires with it; the manual timer-handle cleanup is taken over by structured concurrency
Starting waiting logic from inside a custom Function — impossible, Delay won't go in there spawn, from inside any function including an event callback spawn is the only one of the five concurrency expressions allowed on a wire that can't wait; inside next lesson's Subscribe callback it is the only way out

Where does the difference come from? In the Blueprint execution model, "a wire" is a line you drew — the engine has no object you can name, cancel, or wait on. Verse has the notion of a task at the language level, which is what makes "cancel the losers" and "wait for everyone" expressible as keywords instead of something you re-assemble by hand every time.

It cuts the other way too: a Blueprint Event Dispatcher is a one-to-many broadcast by nature, and adding listeners is just dragging more wires. To do the same in Verse you declare the event and subscribe to it yourself — which is precisely what the next lesson is about.

6. Level Challenges

Three quick challenges to certify your clone-commanding skills. Wrong answers cost nothing — retry as often as you like.

When does the losing side of a race actually stop?

Inside the response you bind to a device event (Subscribe) — a regular wire that can't wait — you want to start some "waiting" logic. Which do you reach for?

rush and race both move on when the first wire finishes. What's the difference?

Further Reading

Technique · EXTRA

Racing a Dynamic Number of Tasks: the Event-Aggregation Pattern

race's arm count is fixed when the graph is wired, so how do you wait for "whichever of N buttons gets pressed first"? The community's battle-tested standard: spawn sentinels + event aggregation.

Open the extra →

Advanced · EXTRA

Cancellation and defer: the Other Half of Structured Concurrency

When and how does a race loser actually exit? Why is defer the standard tool for concurrency cleanup? One small experiment makes the cancellation semantics click.

Open the extra →

Deep Dive · EXTRA

task and awaitable: What spawn Actually Returns

spawn hands you a receipt: the task object. How do you Await it? Why is there no Cancel()? The standard way to call a clone off.

Open the extra →