Timers & async work
Most "do this later / every N seconds" needs belong in a timer, not Tick. For
CPU-intensive work, move computation off the game thread using Async or the Tasks System,
then marshal results back — UObjects and actors must only be touched on the game thread.
When to use this skill
- A delay ("respawn in 3 s") or repeating callback ("regen every 0.5 s").
- Replacing a per-frame accumulator with a world-time-aware periodic timer.
- Deferring one frame (
SetTimerForNextTick), e.g. waiting for another actor to finish spawning before continuing initialization. - Offloading expensive work (procedural gen, parsing, path pre-computation) to a worker thread and applying the result on the game thread.
- Ticking a non-actor subsystem without a
UActorComponent(FTSTicker).
Timers — FTimerManager
FTimerManager manages all gameplay timers for a UWorld. Actors reach it via
GetWorldTimerManager(); non-actor code uses GetWorld()->GetTimerManager() or the
global instance on UGameInstance.
Setting a timer
// In actor header:
FTimerHandle RegenHandle;
// In BeginPlay — looping every 0.5 s:
GetWorldTimerManager().SetTimer(
RegenHandle,
this, &AMyHero::OnRegen,
0.5f,
/*bLoop*/ true);
// One-shot after 3 s:
FTimerHandle RespawnHandle;
GetWorldTimerManager().SetTimer(
RespawnHandle,
this, &AMyHero::OnRespawn,
3.f,
/*bLoop*/ false);
// Lambda variant — first delay differs from repeat rate:
FTimerHandle WarmupHandle;
GetWorldTimerManager().SetTimer(
WarmupHandle,
[this]{ OnWarmupComplete(); },
1.f,
/*bLoop*/ false,
/*FirstDelay*/ 2.f); // fires at 2 s, not 1 s
Keep FTimerHandle as a member so you can cancel or query the timer later. Calling
SetTimer on an already-valid handle cancels the old timer and starts a fresh one.
Next-tick scheduling
// Defer one frame — no handle returned; cannot be cancelled:
GetWorldTimerManager().SetTimerForNextTick(this, &AMyActor::AfterSpawn);
Querying and cancelling
GetWorldTimerManager().IsTimerActive(RegenHandle); // true if running and not paused
GetWorldTimerManager().GetTimerRemaining(RegenHandle); // seconds until next fire; -1 if invalid
GetWorldTimerManager().GetTimerElapsed(RegenHandle);
GetWorldTimerManager().PauseTimer(RegenHandle);
GetWorldTimerManager().UnPauseTimer(RegenHandle);
GetWorldTimerManager().ClearTimer(RegenHandle); // stops and invalidates the handle
GetWorldTimerManager().ClearAllTimersForObject(this); // clears every timer bound to this
ClearTimer invalidates the handle. Passing a rate <= 0 to SetTimer is equivalent to
ClearTimer.
Mandatory EndPlay cleanup
virtual void EndPlay(const EEndPlayReason::Type Reason) override
{
Super::EndPlay(Reason);
GetWorldTimerManager().ClearTimer(RegenHandle);
GetWorldTimerManager().ClearTimer(RespawnHandle);
}
Clear every looping timer in EndPlay. A looping timer that outlives its bound object
will attempt to call a dangling pointer. EndPlay covers all exit reasons (destroy,
level unload, PIE end) — see ue-actors-and-components.
How timers interact with game time
Timers advance on world time, so they automatically respect WorldSettings time
dilation, pausing (SetPause), and slow-motion. They do not fire more than once per
game frame even if the accumulated delta exceeds the rate (modulo bMaxOncePerFrame on
FTimerData). The game-thread-only note in the engine docs is accurate: FTimerManager
is not thread-safe; never set or clear timers from a background thread.
Full reference: references/timer-manager.md.
Async work off the game thread
Async / AsyncTask (quick lambdas)
#include "Async/Async.h"
// Fire heavy work on the thread pool; marshal result back to the game thread:
Async(EAsyncExecution::ThreadPool, [Payload]()
{
// Worker thread — NO UObject/actor/component access:
const FResult R = ComputeHeavyResult(Payload);
// Marshal back:
AsyncTask(ENamedThreads::GameThread, [R]()
{
// Game thread — safe to modify actors, components, UObjects:
ApplyResult(R);
});
});
Async returns a TFuture<T> that lets callers poll or wait for the result. Prefer
EAsyncExecution::ThreadPool for short-to-medium work; use EAsyncExecution::Thread for
long-running work that must not block the pool.
// Capture a UObject safely across threads:
TWeakObjectPtr<AMyActor> WeakSelf(this);
Async(EAsyncExecution::ThreadPool, [WeakSelf, Data]()
{
FResult R = DoWork(Data);
AsyncTask(ENamedThreads::GameThread, [WeakSelf, R]()
{
if (AMyActor* Self = WeakSelf.Get()) // re-validate on game thread
{
Self->ApplyResult(R);
}
});
});
Never capture a raw UObject* or AActor* for use on another thread — the object
can be garbage-collected while the lambda is in flight. Capture a TWeakObjectPtr and
call .Get() after you are back on the game thread.
FNonAbandonableTask / FAutoDeleteAsyncTask
For reusable, structured background tasks with their own data:
// Declare the task work class:
class FMyProcessTask : public FNonAbandonableTask
{
friend class FAutoDeleteAsyncTask<FMyProcessTask>;
TArray<FVector> Points;
explicit FMyProcessTask(TArray<FVector>&& InPoints)
: Points(MoveTemp(InPoints)) {}
void DoWork()
{
// Worker thread — pure computation, no UObjects:
ProcessPoints(Points);
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyProcessTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
// Launch — task self-deletes on completion:
(new FAutoDeleteAsyncTask<FMyProcessTask>(MoveTemp(SomePoints)))->StartBackgroundTask();
When you need to wait for completion or retrieve the result, use FAsyncTask<T> instead,
which exposes EnsureCompletion() and IsDone().
Full reference: references/async-and-tasks.md.
UE Tasks System (UE 5.1+, preferred for new code)
The modern Tasks System (UE::Tasks) builds on the same worker-thread backend as the
task graph but with a cleaner API, dependency graphs, and pipes.
#include "Tasks/Task.h"
using namespace UE::Tasks;
// Fire and forget:
Launch(UE_SOURCE_LOCATION, []{ DoWork(); });
// Capture result:
TTask<int32> Task = Launch(UE_SOURCE_LOCATION,
[]{ return ComputeValue(); });
// Wait and retrieve (blocks calling thread):
int32 Val = Task.GetResult();
// Dependency chain: B runs after A completes:
FTask A = Launch(UE_SOURCE_LOCATION, []{ StepOne(); });
FTask B = Launch(UE_SOURCE_LOCATION, []{ StepTwo(); }, A);
// Pipe: sequential non-concurrent access to a shared resource:
FPipe ResourcePipe{ TEXT("MyResourcePipe") };
FTask T = ResourcePipe.Launch(UE_SOURCE_LOCATION,
[this]{ Resource.Mutate(); });
Prefer this over raw Async/AsyncTask for new code that needs DAG-style dependencies
or serialized access to a shared resource.
Full reference: references/async-and-tasks.md.
FRunnable / FRunnableThread (long-running dedicated threads)
For long-running services (audio streaming, network I/O, simulation loops) that must own a dedicated OS thread:
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
class FMyWorker : public FRunnable
{
public:
FMyWorker() : bStop(false) {}
virtual bool Init() override { return true; }
virtual uint32 Run() override
{
while (!bStop)
{
DoIterationWork(); // never touch UObjects here
FPlatformProcess::Sleep(0.01f);
}
return 0;
}
virtual void Stop() override { bStop = true; }
virtual void Exit() override {}
private:
TAtomic<bool> bStop;
};
// Ownership pattern — actor creates and destroys:
FMyWorker* Worker = nullptr;
FRunnableThread* Thread = nullptr;
void AMyActor::BeginPlay()
{
Super::BeginPlay();
Worker = new FMyWorker();
Thread = FRunnableThread::Create(Worker, TEXT("MyWorker"));
}
void AMyActor::EndPlay(const EEndPlayReason::Type Reason)
{
Super::EndPlay(Reason);
if (Thread) { Thread->Kill(/*bWait*/ true); delete Thread; Thread = nullptr; }
delete Worker; Worker = nullptr;
}
Full reference: references/threads-and-runnables.md.
FTSTicker — non-actor periodic ticking
FTSTicker provides a periodic callback for subsystems and objects that do not have a
UActorComponent. It replaces the older FTicker (removed in UE5).
#include "Containers/Ticker.h"
// Register: return true to keep ticking, false for one-shot:
FTSTicker::FDelegateHandle TickHandle =
FTSTicker::GetCoreTicker().AddTicker(
TEXT("MySubsystemTick"),
0.25f, // delay between fires (seconds)
[this](float DeltaTime) -> bool
{
PollSubsystem(DeltaTime);
return true; // keep ticking
});
// Unregister (e.g. in destructor or shutdown):
FTSTicker::RemoveTicker(TickHandle);
FTSTickerObjectBase is a convenience base class — subclass it and override Tick(float)
instead of managing the handle manually.
Full reference: references/tickers-and-latent.md.
Thread-safety rules (critical)
- Never read or write
UObject/AActor/UActorComponentstate off the game thread. This includesGetWorld(), spawning, delegate broadcast, and GC-tracked pointers. - Capture copies of plain data (structs,
int32,float) into lambdas that cross threads. CaptureTWeakObjectPtr<T>for any UObject; validate with.Get()after returning to the game thread. - Protect mutable non-UObject state shared between threads with
FCriticalSection/FScopeLock(HAL/CriticalSection.h,Misc/ScopeLock.h). FTimerManageritself is game-thread-only; set/clear timers only from the game thread.FTSTicker::AddTickeris thread-safe (the callback fires on the game thread); theFTSTicker::RemoveTickercall blocks until any in-progress callback finishes.
Choosing the right mechanism
| Need | Mechanism |
|---|---|
| Delay or repeat at a fixed cadence | FTimerManager::SetTimer |
| Defer exactly one frame | SetTimerForNextTick |
| Per-frame smooth interpolation | Tick (enable selectively) |
| Short background work, fire-and-forget | Async(EAsyncExecution::ThreadPool, ...) |
| Background work with result / dependencies | UE::Tasks::Launch |
| Reusable background task class | FNonAbandonableTask + FAutoDeleteAsyncTask |
| Long-running dedicated OS thread | FRunnable + FRunnableThread |
| Non-actor periodic callback | FTSTicker |
Latent actions (Blueprint async nodes)
Blueprint Delay nodes and latent K2 functions are backed by FPendingLatentAction
(registered on UWorld::GetLatentActionManager()). From C++, prefer timers for actor
logic. If you need a Blueprint-exposed "async node" that shows a white execution pin,
subclass UBlueprintAsyncActionBase instead of implementing a raw FPendingLatentAction.
Gotchas
- Lost
FTimerHandle— a looping timer without a stored handle cannot be cancelled; it runs until the world tears down. - No EndPlay cleanup — a looping timer whose delegate references
thiswill call into freed memory after the actor is destroyed; always clear inEndPlay. - Raw
UObject*captured across threads — GC can collect the object while the lambda is in flight; useTWeakObjectPtrand re-validate on the game thread. - Timer rate
<= 0— silently treated asClearTimer; guard against accidental zero rates when computing a dynamic interval. SetTimerForNextTickhas no handle — it cannot be cancelled; do not call it if the actor might be destroyed before the next frame.- Blocking the game thread on a future — calling
TFuture::Get()orFTask::Wait()from the game thread stalls rendering; only block from worker/background threads or a known safe point (e.g. level loading). FTickervsFTSTicker—FTickerwas removed in UE5; always useFTSTicker.
Version notes
FTSTickerreplacedFTickerin UE5. Any UE4-era code usingFTicker::GetCoreTicker()must be ported toFTSTicker::GetCoreTicker().- The UE Tasks System (
UE::Tasks) was introduced in UE 5.0. Prefer it over direct task-graph usage (TGraphTask) for new code in 5.8. - Busy-waiting in
UE::Taskswas deprecated in UE 5.5 and replaced by oversubscription (standby threads). Do not call the removed busy-wait APIs.
References & source material
Engine source (UE 5.8, under Engine/Source/):
Runtime/Engine/Classes/Engine/TimerHandle.h—FTimerHandle:11.Runtime/Engine/Public/TimerManager.h—FTimerManager:137,SetTimer:167,SetTimerForNextTick:249,ClearTimer:281,PauseTimer:304,UnPauseTimer:311,IsTimerActive:331,GetTimerRemaining:444,ClearAllTimersForObject:291,FTimerManagerTimerParameters:124.Runtime/Core/Public/Async/Async.h—EAsyncExecution:27 (enum withTaskGraph,Thread,ThreadPool,TaskGraphMainThread,TaskGraphMainTick),Async:299,AsyncTask:463.Runtime/Core/Public/Async/AsyncWork.h—FAutoDeleteAsyncTask:60,FAsyncTaskBase:208,FAsyncTask:587,FNonAbandonableTask:666.Runtime/Core/Public/Async/TaskGraphInterfaces.h—ENamedThreads:54 (namespace,GameThread,AnyThread,RHIThread).Runtime/Core/Public/Async/Future.h—TFuture:378,TPromise:527.Runtime/Core/Public/Tasks/Task.h—UE::Tasks::TTask,FTaskalias,Launch,AddNested,Waitinnamespace UE::Tasks.Runtime/Core/Public/Tasks/Pipe.h—UE::Tasks::FPipe:28.Runtime/Core/Public/HAL/Runnable.h—FRunnable:19 (Init,Run,Stop,Exit).Runtime/Core/Public/HAL/RunnableThread.h—FRunnableThread:19,Create:44.Runtime/Core/Public/Containers/Ticker.h—FTSTicker:26,AddTicker:45,RemoveTicker:66,FTSTickerObjectBase:136.Runtime/Core/Public/HAL/CriticalSection.h—FCriticalSection:53 (alias forUE::FPlatformRecursiveMutex).Runtime/Core/Public/Misc/ScopeLock.h—FScopeLock:140.
Official docs (UE 5.8, verified):
- Gameplay Timers — https://dev.epicgames.com/documentation/unreal-engine/gameplay-timers-in-unreal-engine
- Tasks System — https://dev.epicgames.com/documentation/unreal-engine/tasks-systems-in-unreal-engine
Deep-dive references in this skill:
- references/timer-manager.md — FTimerManager internals, delegate variants, timer parameters struct, time-dilation interaction.
- references/async-and-tasks.md — Async/AsyncTask patterns, TFuture/TPromise, FNonAbandonableTask, the UE Tasks System (Launch, FPipe, prerequisites, task events).
- references/threads-and-runnables.md — FRunnable lifecycle, FRunnableThread::Create, thread priorities, stopping safely.
- references/tickers-and-latent.md — FTSTicker API, FTSTickerObjectBase pattern, latent actions overview.