Task Library - Now Available!

Sorry to bother you again, if you were asked for an opnion, what would you say is better? Using this, or a solution like it, like CloneTrooper’s Thread, or using task.wait?

Any explanations as to why it’s made to behave this way, I can see this resulting in unexpected yield frequencies, especially for accurate things that need to be binded to 60 fps (because task.wait behaves in the way you stated on the server as well). So just wanting an explanation is all. Also to add to this task.wait can’t yield for a proper 60 fps step when doing task.wait(1/60) , seems to result in task.wait yielding for about 1/44, a pretty big drop.


Most performance heavy functions are ran in C closures, which means they are implemented in C/C++ to evade the Lua VM’s performance cost.

You can see if a function is a C closure by doing the following:

local func = print -- print is a c closure
print(debug.info(func, "s")) -- 's' for source, returns [C] because it's a C function

From my understanding task.wait() is more like RunService.Heartbeat:Wait() instead of wait().

:Wait() is different from wait() because it’s waiting for the event tied to it to fire. wait() however may take longer to wait than it needs to which is why it should be avoided.

I don’t think it matters if you use RunService.Heartbeat:Wait() or task.wait(). They both do the same thing (from what is told). Personally I would only use RunService.Heartbeat:Wait() if I was using RunService for other things in my code (asynchronous threads and everything) and I specifically needed to wait for the Heartbeat event. I would only do this for code readability. Otherwise I would use task.wait()


We need to resume delayed threads at some point in the engine step and Heartbeat was the logical choice for this as it runs after all other internal engine steps. However, this is an implementation detail. The primary reason to use wait is to yield a script for some amount of time rather than to bind code to the heartbeat step. If you want to bind code to the step then you should definitely be using:

RunService.Heartbeat:Connect(function (stepTime)
  -- do something

This is why wait returns the elapsed time rather than the step time.

Heartbeat fires once every frame. If the engine is running at 60 FPS then the event will fire 60 times a second. If it runs at 40 FPS then the event will fire 40 times a second and so on. Passing a duration of 1/60 does not guarantee the thread will be resumed in one 60th of a second, just that at least one 60th of a second will have passed before the thread is resumed.


It depends what you want. In my estimation, thanks to being throttling-free and the other changes task.wait() should be good enough for almost all use cases.

If you really want to fine tune your game there may still be room for custom scheduling solution built in Lua that does let you throttle execution but in a way that you define, however, almost no-one should need to go to those lengths.


That’s naturally expected given the way the API works.

task.wait() in a loop will always yield 1 frame as an edge case, but you’ll never be able to yield precisely n frames with task.wait(n/60) regardless of how it’s implemented, because not every frame is exactly the same length and you will run into aliasing issues. The only sensible way to do that would really be to have a separate API something like task.waitframes(n) which doesn’t care about wall-clock or in-engine time at all, only frame count.

You could also do this yourself as:

local function waitNFrames(n)
    for i = 1, n do

Sweet, then it’s good to see that it runs at a C closure.

Looks like it’s more performant than thread handlers, I presume!

I read task library, I expected something like… a task manager… for projects in studio. Nope. Still a good feature I for scripters!

coroutine.wrap starts a coroutine under that script. This is different in that it doesn’t create a new context, and if the script is destroyed, the coroutine also automatically dies.

task.spawn and task.defer however create a new thread underneath Roblox’s task handler, which is it’s own script context and wont stop if the calling script is destroyed.

Since you’re passing in a callback (or thread) from the calling script, you still get access to that script’s environment, but internally, the thread is running under a different system.


Thank you for this, this is exaclty what I was looking for ontop of multithreading.

Definitely excited for this, will make coding much more organized for me.

I really love task.spawn making guns now is gonna be a lot better and the players wont feel as much lag

Now people can’t complain that tutorials are deprecated since all tutorials are now deprecated… :joy:

But in all seriousness, very nice to see improvements in these parts. :eyes:

1 Like

Finally, a new way to apprehend such difficulties. With that being said, Roblox adding this new method gives us developers a bigger reputation in problem solving and development in general. Not only is the method itself is suffisticated but it also holds an important role in Programming!

I’m pretty annoyed that the entire post is significantly more than this, but you only clipped that section out.

The task API is great and all, but task.wait(), with no arguments, still has the same code smell as wait(), because it was never about the internals.


Do you mind explaining what “throttling” means?


Only 10% off a frame’s time will be used resuming things which were spawned / which waited. If resuming of those waiting things takes longer than that 10%, the ones in excess of the 10% time budget will be delayed to a future frame even though their timeout has already elapsed. This can be problematic, because by the very end of the frame there may actually be a significant amount of time left over which could have been used resuming more threads on more powerful devices.

task.wait() doesn’t have this throttling, everything you task.wait() will resume exactly on the frame the wait time is up regardless of the consequences (potential lag), which gives you more control.


This new library is a great replacement of wait and other functions, but I experienced a huge problem with it. If you delete a script that has a loop which utilizes task.wait, that loop will continue to run for an infinite amount of time unless it has special additional checks in place to stop itself. Using .RenderStepped:Wait() or standard wait would stop the loop and thus disabled script wont actually run anymore.

While this makes sense from standpoint of how this new library works, its counterintuitive for people who are used to behavior of standard wait, and if not handled properly it can make stuff lag over time (especially if that loop is located in an object that gets destroyed/respawned a lot, like character or vehicles with scripts in them)


Coming back to this post but… I don’t quite understand the use case of task.defer. Anyone able to explain when it would be appropriate to use? I’m especially keen on this because I want to get started on working with Deferred signals so I’m future-proofed when Immediate signals go poof.

I’ve got the hang of most of the other events but deferred stuff confuses me heavily even after checking out the diagram and everything from the other thread. I’m not sure when and where I should use it or what kinds of problems it’d help me solve. Is it stupid to defer everything? When does code not in task.defer run? Just in general I don’t really understand deferring stuff.

EDIT: I did find an interesting use of task.defer. It doesn’t help me understand what exactly deferring is or what task.defer is used for, but I tested it out a bit and made a script that prevents a tool from being unequipped. It works in both Immediate and Deferred signal modes.

local Tool = script.Parent

local Character

Tool.Equipped:Connect(function ()
	Character = Tool.Parent

Tool.Unequipped:Connect(function ()
	if Character then
		task.defer(function ()
			Tool.Parent = Character