Overview
OwlScheduler is a task scheduling utility designed to manage, defer and budget execution time across frames. Instead of cluttering your code with task.wait(), task.spawn() or unbounded RunService connections, OwlScheduler lets you schedule tasks with priority queues, configurable execution budgets and automatic error isolation.
Key Features
- Budget Control: Limit task execution time per frame (in milliseconds) to prevent frame drops.
- Multiple Queues: Separate queues for interval tasks, frame-by-frame updates, client rendering and deferred actions.
- Safe Execution: Every scheduled task runs inside a
pcallwrapper, preventing one failing task from breaking the whole loop. - Flexible Handles: Cancel, inspect or clean up scheduled tasks dynamically using standard
TaskHandleobjects.
Basic Usage
Scheduling a One-Time or Recurring Task
Use :Add() to schedule a simple task or :Every() to run a function at fixed intervals:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Owl = require(ReplicatedStorage.OwlKnit.Owl)
local OwlScheduler = Owl.Util.Scheduler
-- > // Run a task every 2 seconds
local handle = OwlScheduler:Every(2, function(dt)
print("Periodic check triggered after", dt, "seconds")
end, {
Priority = 1,
BudgetCost = 0.1,
})
-- > // Stop the task whenever needed
task.wait(10)
handle:Cancel()Running Logic Until a Condition Ends
Use :Until() when a callback should execute repeatedly until a condition evaluates to true:
OwlScheduler:Until(function(dt)
-- > // Returning true automatically cancels and unqueues the task
if characterIsDead() then
return true
end
updateHealthBar(dt)
return false
end)Task Lifecycle & Handles
Whenever you add a task, OwlScheduler returns a TaskHandle. You can use this handle to check its running state or cancel it early without keeping track of connection objects:
local handle = OwlScheduler:Add(function()
print("Executing task...")
end, { Delay = 5 })
if handle:IsRunning() then
handle:Cancel() -- > // Task will be cleaned up on the next cycle
endQueues & Budgeting
To prevent execution spikes from causing lag, OwlScheduler categorizes tasks into four distinct queues and enforces a strict millisecond budget during frame updates.
Available Queues
Each queue targets a specific phase of the Roblox lifecycle:
| Queue Method | Targeted Hook | Description |
|---|---|---|
:Add() / :Every() |
Heartbeat |
General task queue subject to the millisecond budget limit. |
:NextFrame() |
Heartbeat |
High-priority queue executed once at the start of the next frame (unbudgeted). |
:OnRender() |
RenderStepped |
Client-only queue executed right before rendering each frame. |
:Defer() |
Heartbeat |
Low-priority queue drained after main tasks, subject to budget limits. |
Budgeting Mechanics
By default, OwlScheduler allocates 2 ms per frame (DefaultBudgetMS) for standard and deferred queues.
During each Heartbeat, OwlScheduler drains the frame queue first, then checks whether remaining time fits the task's estimated BudgetCost before executing it:
-- > // Adjust the global execution budget to 4 ms
OwlScheduler:SetBudget(4)
OwlScheduler:Every(0.5, function()
-- > // Heavy task execution
end, {
BudgetCost = 0.5, -- > // Estimated millisecond cost
})If a task's
BudgetCostexceeds the remaining frame budget, execution for that queue pauses until the nextHeartbeatframe.
Queue Priority
Within each queue, tasks are processed according to their Priority property (lower numbers execute first):
OwlScheduler:Add(function()
print("Executes second")
end, { Priority = 10 })
OwlScheduler:Add(function()
print("Executes first")
end, { Priority = 1 })Monitoring Performance
You can query performance statistics at runtime using :GetStats() to monitor frame times and queue sizes:
local stats = OwlScheduler:GetStats()
print(("Last Frame: %.2f ms | Avg Frame: %.2f ms"):format(stats.LastFrameMs, stats.AverageFrameMs))
print(("Task Queue Length: %d"):format(stats.TaskQueueLength))API Reference
This section details all types, options, and methods available in OwlScheduler.
Type Definitions
TaskFn
type TaskFn = (dt: number) -> boolean?A callback function that receives the delta time (dt). Returning true will automatically cancel the task.
TaskOptions
type TaskOptions = {
Priority: number?, -- > // Lower numbers run first (default: 0)
Delay: number?, -- > // Delay in seconds before first execution
Interval: number?, -- > // Interval in seconds between executions
Repeat: number?, -- > // Maximum number of execution cycles
BudgetCost: number?, -- > // Estimated time cost in ms (default: 0.05)
}TaskHandle
type TaskHandle = {
Cancel: (self: TaskHandle) -> (),
Destroy: (self: TaskHandle) -> (),
IsRunning: (self: TaskHandle) -> boolean,
}SchedulerStats
type SchedulerStats = {
BudgetMs: number,
LastFrameMs: number,
AverageFrameMs: number,
TaskQueueLength: number,
FrameQueueLength: number,
RenderQueueLength: number,
DeferredQueueLength: number,
}Methods
:Add(fn, opts)
Enqueues a task into the main task queue.
- Parameters:
fn:TaskFnopts:TaskOptions?
- Returns:
TaskHandle
:Every(interval, fn, opts)
Schedules a task to run repeatedly at a fixed interval.
- Parameters:
interval:number(must be non-negative)fn:TaskFnopts:TaskOptions?
- Returns:
TaskHandle
:NextFrame(fn, opts)
Schedules a task to run once on the very next Heartbeat frame.
- Parameters:
fn:TaskFnopts:TaskOptions?
- Returns:
TaskHandle
:OnRender(fn, opts)
Schedules a task to run during RenderStepped. Client only.
- Parameters:
fn:TaskFnopts:TaskOptions?
- Returns:
TaskHandle
:Defer(fn, opts)
Schedules a task in the deferred queue to be processed after main tasks.
- Parameters:
fn:TaskFnopts:TaskOptions?
- Returns:
TaskHandle
:Until(fn, opts)
Executes a function every frame until it returns true.
- Parameters:
fn:(dt: number) -> booleanopts:TaskOptions?
- Returns:
TaskHandle
:SetBudget(ms) / :GetBudget()
Gets or sets the execution budget per frame in milliseconds.
- Parameters:
ms:number(> 0)
:GetStats()
Returns a SchedulerStats object with active queue lengths and performance measurements.
- Returns:
SchedulerStats