Skip to content
Engine

Runtime

ComputeService

Run heavy work in parallel on Roblox Actors with worker pools, parallel for-loops, dependency graphs and shared tables, all returning Promises.

ServiceShared

Getting it

local ComputeService = Engine:GetService("ComputeService")

Overview

Roblox can run Luau on several CPU cores at once through Actors, but setting that up by hand means managing actor scripts, messages and results. ComputeService does it for you: it keeps a pool of worker actors, sends them work, and gives you a Promise with the result.

Work is written as a task module: a ModuleScript that returns one function. That function runs in parallel, so it must follow Roblox's parallel rules (it can read the DataModel and do maths, but cannot change most instances).

Task module: ReplicatedStorage.Tasks.HeightAt
local Noise = require(path.to.your.noise) -- anything that is safe in parallel

return function(index: number, size: number)
	local x = (index - 1) % size
	local z = (index - 1) // size
	return math.noise(x / 50, z / 50) * 40
end
Using it
local ComputeService = Engine:GetService("ComputeService")

local SIZE = 512
ComputeService:ParallelForAsync(SIZE * SIZE, ReplicatedStorage.Tasks.HeightAt, SIZE)
	:andThen(function(heights)
		buildTerrain(heights) -- back in serial, safe to change instances
	end)

How many workers

When first used, ComputeService runs a short capacity probe: it measures how many actors this machine can run at the same time before they stop getting faster, and sizes pools to match. Until the probe finishes, DefaultMaxWorkers is used. On servers it re-checks when players join (at most once per CapacityRecheckCooldownSeconds).

Workers live in a folder named Engine_ComputeService_Workers under ServerScriptService (server) or the player's PlayerScripts (client).

Parallel for

ParallelForAsync

ComputeService:ParallelForAsync(count: number, jobModule: ModuleScript, shared: any?): Promise<{ any }>

Calls the job function as fn(i, shared) for every i from 1 to count, split into one chunk per worker. Resolves with the results in order. shared is sent to every worker; for large data pass a shared table's Raw (see Shared tables) instead of copying a big table to each worker.

Worker pools

CreatePool

ComputeService:CreatePool(name: string, config: WorkerPoolConfig?): WorkerPool

A named pool for your own tasks. Names starting with __ are reserved.

ConfigDescription
MaxWorkersMost workers at once. Default: the detected capacity.
IdleTimeoutSeconds an idle worker lives before it is destroyed.
IdleSweepIntervalSeconds between idle checks.
PrewarmWorkers to create up front. Default: MaxWorkers.

GetPool / DestroyPool

ComputeService:GetPool(name: string): WorkerPool?
ComputeService:DestroyPool(name: string)

WorkerPool

MemberDescription
:RegisterTask(name, moduleScript)Makes a task module available to the pool's workers.
:RunAsync(taskName, ...): PromiseRuns the task once with the arguments on a free worker and resolves with its return values. Waits in a queue when every worker is busy.
:RunRangeAsync(taskName, count, extra?): Promise<{ any }>Like ParallelForAsync on this pool: fn(i, extra) for 1 to count.
:RunAllAsync(taskName, argsList): Promise<{ any }>One RunAsync per entry of argsList (each an array of arguments), resolved together.
:Prewarm(count): numberCreates workers ahead of time and returns how many were made.
:Start(), :Stop()Start idle sweeping; stop and destroy all workers. Running a task starts the pool by itself.
:Destroy()Stops the pool for good.
:GetStats(): PoolStatsWorkerCount, BusyCount, IdleCount, QueueLength, TotalTasksRun, TotalTaskErrors.
MaxWorkers, IdleTimeoutSettable.
Name, WorkerCount, BusyCount, IdleCount, QueueLength, DestroyedRead-only.
WorkerCreated, WorkerDestroyed, TaskFailedSignals.
local pool = ComputeService:CreatePool("Pathing", { MaxWorkers = 4 })
pool:RegisterTask("FindPath", ReplicatedStorage.Tasks.FindPath)

pool:RunAsync("FindPath", startCell, goalCell):andThen(function(path)
	npc:Follow(path)
end)

An error inside a task rejects that task's Promise; the worker survives and takes the next task.

Task graphs

CreateGraph

ComputeService:CreateGraph(): TaskGraph

A set of tasks with dependencies. Tasks whose dependencies are done run in parallel; each task receives the results of the tasks it depends on as arguments, in the order you listed them.

local graph = ComputeService:CreateGraph()
	:AddTask("Heights", Tasks.Heights)
	:AddTask("Moisture", Tasks.Moisture)
	:AddTask("Biomes", Tasks.Biomes, { "Heights", "Moisture" })

graph:RunAsync():andThen(function(results)
	paintBiomes(results.Biomes)
end)
MethodDescription
:AddTask(name, moduleScript, dependsOn?)Adds a task. Returns the graph, so calls chain.
:HasTask(name), :GetTaskNames()
:RunAsync(pool?): Promise<{ [name]: result }>Runs everything and resolves with every result by name. Rejects with the failing task's name and error. A graph with a cycle or a missing dependency rejects before anything runs.
:Describe(): stringThe graph as Graphviz DOT.

Shared tables

CreateShared / GetShared

ComputeService:CreateShared(name: string, initial: { [any]: any }?): SharedHandle
ComputeService:GetShared(name: string): SharedHandle

A named SharedTable that every actor can read without copying. Inside a task module, read it with SharedTableRegistry:GetSharedTable("ComputeService:" .. name), or pass the handle's Raw as an argument.

MemberDescription
:Get(key), :Set(key, value)Read and write.
:Update(key, fn)Atomic update with fn(old).
:Increment(key, delta?): numberAtomic add, returns the new value.
:Snapshot(deep?)A normal Luau table copy.
:Clear()
:Freeze(deep?)Replaces it with a frozen copy, which is faster to read from many actors.
Name, Size, Frozen, RawRead-only.

Diagnostics

MethodDescription
:GetStats(){ PoolCount, TotalWorkers, TotalBusy, TotalQueued } over every pool.
:GetActorCount()Workers alive right now.
:GetMaxActorCount()The detected capacity (or the default until detection finishes).
:RecheckCapacityAsync(): Promise<number>Runs the capacity probe again.

Configuration

Defaults from DefaultConfiguration/Services/ComputeService7 settings
SettingDefaultWhat it does
DefaultMaxWorkers16Bootstrap worker ceiling used until the capacity probe has measured the real one.
DefaultIdleTimeout45Seconds a worker can sit idle before it is destroyed.
IdleSweepInterval15Seconds between idle-worker sweeps.
CapacityIterationsPerActor250000Trig iterations each actor runs per capacity-probe trial.
CapacityMaxProbe64Largest actor count the capacity probe will ever try.
CapacityTolerance1.6How far a trial's wall time may drift above the N=1 baseline before that N is rejected.
CapacityRecheckCooldownSeconds60Minimum seconds between two PlayerAdded-triggered capacity rechecks (server only).

Types

export type WorkerPool
export type WorkerPoolConfig = { MaxWorkers: number?, IdleTimeout: number?, IdleSweepInterval: number?, Prewarm: number? }
export type PoolStats
export type TaskGraph
export type SharedHandle
export type Stats = { PoolCount: number, TotalWorkers: number, TotalBusy: number, TotalQueued: number }