The Task Scheduler

The Task Scheduler

15 min

The Task Scheduler coordinates all the work to be done each frame as the game runs, even when the game is paused. Work to be done includes detecting player input, animating characters, updating the physics simulation and resuming scripts that are wait-ing. It’s important to not overload the task scheduler to your game running smoothly. Especially if you are:

  • Using a custom character rig or input scheme
  • Animating parts yourself (instead of using an Animator)
  • Heavily dependent on precise physics
  • Replicating objects regularly


A frame is a unit of game logic in which work is done. Many frames occur in a game every second, and generally more frames per second (fps) means better performance. The more efficiently work is done each frame, the more frames you can have per second, and the better the experience will be for players.


The most direct way to add frame-by-frame tasks to your game is to use the following members of RunService:

  • RunService/BindToRenderStep
  • RunService/RenderStepped
  • RunService/Stepped
  • RunService/Heartbeat

Choosing which member to use must be done with care.

What does the Task Scheduler do every frame?

The Task Scheduler categorizes and completes tasks in the following order. Some tasks may not do any work in a frame and others may do their work multiple times.

  1. User Input: Events related to input (UserInputService) and bound functions (ContextActionService/BindAction) are handled first: key presses, mouse movements, touches, gamepad vibration, etc.
  2. Rendering
    1. RunService/BindToRenderStep callbacks: All functions bound this way are called in priority order. In the built-in PlayerModule, the camera and character control scripts bind callbacks at certain priorities determined by Enum.RenderPriority.
    2. RunService/RenderStepped: This event is fired. As with all events, connected functions are called, with the most recently connected callback run first.
    3. Screen drawing: At this point, the game state is rendered in parallel. Any further changes will not be rendered until the next frame.
  3. Replication Receive Jobs: Incoming property changes and event firings are applied.
  4. wait resume:
    1. Any running scripts that are currently waiting where enough time has passed will resume.
    2. There is a minimum wait resume time of 0.03s, or about 1/30th of a second.
    3. Functions passed to spawn and delay also run here.
    4. If there are still script threads that need to resume and this step has taken longer than 0.1s, they will be resumed in a later frame.
  5. Lua Garbage Collection
  6. Simulation Job: This step is skipped if the game is paused.
    1. Internal Legacy Step:
      1. Explosions: Terrain damage, knockback, breaking joints
      2. BodyMovers: BodyPosition, RocketPropulsion, BodyGyro, etc.
      3. Humanoid: state updates occur, such as movement or death.
      4. Internal Animation Step: Animators update Motor6D.Transform. This does not move parts or update the applied joint offset.
    2. RunService/Stepped: This event is fired.
    3. Internal Physics Step:
      1. Motor6D: Parts joined by Motor6D will have their relative positions updated.
      2. World Step: Updates part contacts and solve contacts and constraints. This can happen multiple times per frame, since physics will update at 240 Hz. BasePart/Touched and related physics events will fire after all world steps are complete.
      3. Interpolate Network Assemblies: Interpolate the positions of bodies that are not owned (i.e. BasePart/SetNetworkOwnership).
  7. RunService/Heartbeat: This event is fired.
  8. Replication Send Jobs: Outgoing property updates and event firings are sent. Does not happen every frame.

Rules of Thumb

The following guidelines may prove useful when creating your game with efficiency in mind:

  1. Don’t bind functions to render step (BindToRenderStep/RenderStepped) unless absolutely necessary. Doing so delays the render thread which reduces the game’s FPS. Only tasks that must be done after input but before rendering should be done here, such as camera movement. For strict control over order, use RunService/BindToRenderStep|BindToRenderStep instead of RunService/RenderStepped|RenderStepped.
  2. The fewer scripts using wait at any given time, the better.
    1. Avoid using while wait() do … end or while true do wait() end constructs, as these aren’t guaranteed to run exactly every frame or gameplay step. Use events like RunService/Stepped|Stepped or RunService/Heartbeat|Heartbeat, as these events strictly adhere to the core task scheduler loop.
    2. Similarly, avoid using spawn or delay as they use the same internal mechanics as wait. Uses of spawn are generally better served with the coroutine library: coroutine.wrap and coroutine.resume.
  3. In regards to RunService, RunService/Stepped|Stepped happens before physics, RunService/Heartbeat|Heartbeat happens after physics, so therefore:
    1. Gameplay logic that affects physics state should be done in Stepped, such as setting BasePart/Velocity|Velocity of parts.
    2. Gameplay logic that relies on or reacts to physics state should be handled in Heartbeat, such as reading BasePart/Position|Position of parts to detect when they enter defined zones.
  4. If you are changing Motor6D/Transform, this should always be done in RunService/Stepped as Animators will overwrite changes on the next frame if done at any other time. Even without an Animator, Stepped is the last Lua event fired before Motor6D.Transform is applied to part positions.