Runtime
SchedulerService
Spread heavy work over frames with time budgets, priority queues, deferred calls and throttling, so the game never hitches.
Getting it
local SchedulerService = Engine:GetService("SchedulerService")Overview
A frame lasts about 16 milliseconds. Doing too much work in one frame (spawning 500 NPCs, rebuilding a map, recalculating a leaderboard) causes a visible hitch. SchedulerService lets you hand work to the engine with a budget in milliseconds per frame; it does as much as fits each frame and continues on the next.
It has three tools:
- Queues: named, long-lived lists of jobs with a per-frame budget and a priority.
- SpreadAsync: process one array over as many frames as needed, returning a Promise.
- Defer and Throttle: run something a few frames later, or at most N times per second.
local SchedulerService = Engine:GetService("SchedulerService")
SchedulerService:SpreadAsync(spawnPoints, function(point)
spawnTree(point)
end, { budgetMs = 3 }):andThen(function()
print("forest ready")
end)Every queue and spread runs on a RunService event, the phase. The default is Heartbeat. You can use any RunService event the current side has, such as PreSimulation, PostSimulation, or on the client PreRender.
Queues
CreateQueue
SchedulerService:CreateQueue(name: string, config: { budgetMs: number?, priority: number?, phase: string? }?): QueueCreates a named queue. Queue names are unique; creating a second queue with the same name errors (use GetQueue). When several queues share a phase, lower priority numbers run first.
local pathing = SchedulerService:CreateQueue("NpcPathing", { budgetMs = 1.5, priority = 1 })
for _, npc in npcs do
pathing:AddJob(function()
npc:RecomputePath()
end)
end
pathing.Drained:Connect(function()
print("all paths updated")
end)GetQueue
SchedulerService:GetQueue(name: string): Queue?Queue
| Member | Description |
|---|---|
:AddJob(fn): Job | Adds a job. The returned Job has :Cancel(), which skips it if it has not run yet. |
:Clear() | Drops every waiting job. |
:Destroy() | Clears the queue and removes it from the scheduler. |
Name, Phase | Read-only. |
BudgetMs: number | Milliseconds per frame. Can be changed; must be positive. |
Priority: number | Can be changed; queues are re-sorted. |
Paused: boolean | A paused queue keeps its jobs but runs none. |
JobCount, MsUsedLastFrame, Destroyed | Read-only. |
Drained: Signal | Fires when the queue runs out of jobs. |
JobErrored: Signal<err> | Fires when a job errors. Other jobs keep running. |
Jobs should not yield: a job that waits holds up the queue (and every queue after it in that phase) until it resumes.
GetFrameReport
SchedulerService:GetFrameReport(): { [string]: number }Milliseconds each queue used last frame, by name. Handy for a debug overlay.
BudgetExceeded
SchedulerService.BudgetExceeded: Signal<(queueName: string, msUsed: number)>Fires when a queue used more than its budget in a frame. A single job longer than the budget always overshoots, because a job is never cut off halfway.
Spreading work
SpreadAsync
SchedulerService:SpreadAsync(items: { any }, fnPerItem: (item, index) -> (), config: { budgetMs: number?, perFrame: number?, phase: string? }?): PromiseCalls fnPerItem for every item, using up to budgetMs per frame and, if set, at most perFrame items per frame. Returns a Promise that resolves when every item is done. Cancelling the Promise stops the work. An item that errors is reported with warn and the rest continue.
Timing helpers
Defer
SchedulerService:Defer(fn: () -> (), frames: number?): DeferHandleCalls fn after frames Heartbeat frames (default 1). The handle has :Cancel().
Throttle
SchedulerService:Throttle(fn, hz: number, options?): (throttledFn, ThrottleControl)Returns a wrapped function that runs fn at most hz times per second. This is RateLimiter.Throttle with a rate instead of an interval, and it takes the same options and returns the same control object.
local updateMinimap = SchedulerService:Throttle(function(position)
minimap:Draw(position)
end, 10)
RunService.RenderStepped:Connect(function()
updateMinimap(camera.CFrame.Position)
end)Configuration
Values passed in a call win over these defaults. See Configuration.
DefaultConfiguration/Services/SchedulerService5 settings| Setting | Default | What it does |
|---|---|---|
| DefaultBudgetMs | 2 | Milliseconds a Queue may spend per Phase tick before yielding. |
| DefaultPriority | 0 | Lower runs first when multiple Queues share a Phase. |
| DefaultPhase | "Heartbeat" | RunService event driving a Queue/Spread when none is given. |
| DefaultSpreadBudgetMs | 2 | Milliseconds :SpreadAsync() spends per tick when config.budgetMs is omitted. |
| DefaultDeferFrames | 1 | Frames :Defer() waits when none is passed. |
Types
export type Queue
export type Job
export type QueueConfig = { budgetMs: number?, priority: number?, phase: string? }
export type SpreadConfig = { budgetMs: number?, perFrame: number?, phase: string? }
export type DeferHandle = { Cancel: (self: DeferHandle) -> () }