[VIDEO] How to not use WHILE WAIT() loops

How to not use while wait() loops

I’m aware many devs are looking to optimize their code as much as this can, and this is a great place to start.

In a sense, the wait-loop is replaced with a RunService-related alternative.

Hope this helps, be sure to ask questions if you need to ^-^


This is a good video but there’s one piece of misinformation. Stepped isn’t less accurate than Heartbeat. Stepped fires every frame before physics are simulated, and heartbeat fires every frame after physics are simulated. They’ll both fire 60 times a second (assuming there isn’t any lag happening).

I would like to add on that there’s open source modules with custom wait functions that use Heartbeat/Stepped to wait without the inaccuracies of the wait() function, so they can act as a replacement for while true do wait loops. Here’s the one I use.

The code at the end could be replaced with this using the module I linked, and you’d get the same result.

while true do

The video is not what the title is, you’re just creating a additional thread which is unnecessary and expensive.

This is how you would do it without creating a new thread:

local RunService = game:GetService('RunService')

while (true) do


I don’t see any point of this tutorial and the video…

1 Like

I ran some benchmarks of the script in the video to the example shown in (accumulator) Using DeltaTime

To the YouTube method:

local RunService = game:GetService("RunService")
local Timer = 0
local TimerMax = .1
	Timer += step
	if Timer >= TimerMax then
		Timer = 0
		print("youtube video method")

And the examples in the video doesn’t know how to take lag into consideration (notice the prints at the start when you start the server how they stack when there’s lag but the YouTube method doesn’t do that until the server is running somewhat stable).

I don’t know if you should expand the tutorial on this because it will skip if there’s lag. Do it if you want. I mean what the video shows is expected behavior and it’s faster than while wait() loops if you lower the rate.


You have to keep in mind that not all usages of wait(number) are bad. Rather the incorrect usages of them is.

The problems which occur because of WaitingScriptTask arise from the incorrect usages of waits and related functions.
Nearly all issues stem from either using spawn(FUNC, ARGS) instead of coroutine.wrap(FUNC)(ARGS) , Not using deltatime and busy waits.

  • Do not use spawn(FUNC) use coroutine.wrap(func)() instead

  • DO NOT USE BUSY WAITS! Always use events!!! Busy wait means when you have a loop which waits something to happen. For example repeat wait() until script.Parent:FindFirstChild("Human") could be replaced by script.Parent:WaitForChild("Human") , this pathfinding waiter repeat (Destination.Position - script.Parent.PrimaryPart.Position).Magnitude < 0.5 could be replaced by humanoid.MoveToFinished:Wait() So basically when you want to wait for an event to happen just do EVENT:Wait() See Avoiding wait() and why for more info. Also Instance:GetPropertyChangedSignal is really useful when you want an event for a change of a specific property.

  • Learn to use delta time. Using DeltaTime .
    Note you do not have (and probably shouldn’t) use runservice wait events. Instead you can use wait(number) to get the delta and it would work fine.
    The places where you would want to use delta time is any place where there is some loop and something is incremented or decremented. Like for i = 1, AmountOfTimes do wait() VAL = VAL + ADD end or while true do wait() VAL = VAL + ADD end In these cases you must always use delta time. Also remember to use math.clamp(VAL, MIN, MAX) To prevent the delta from being over the desired end goal.

  • Only use runservice waits when you absolutely need. You probably do not have to have a wait down to the precision of milliseconds or a super fast wait. If you do, you are doing something wrong. But for really important stuff like how long a match lasts you can use a heartbeat based wait but only use sparingly at the most critical parts of your game if you known you need to. Also when you use a wait not to make a loop crash Runservice.Hearbeat is probably for you.

  • Some visuals and some really important stuff can use the runservice events. But polling waits with runservice is a bad practice usually so do not use it outside of really critical things.

  • Do not use tick() when getting time difference. Use os.clock().

Using delay() wait(number) and Debris:AddItem() is ok. Just don’t use spawn(). And remember not to use busy waits and use delta. Also you can in rare cases use runservice based waits like when detecting the length of a match or something but everything else can use wait(number).

Also see Task Scheduler for more useful info.


No, creating a new thread from a RunService event is not expensive, not unless the code itself is expensive. If it is expensive, then you did something wrong.


Completely false, creating a new thread in general is expensive.You’re passing the callback to Connect which then calls the callback function passed to it in a new thread.

wait() in extreme situations can take up to 1 extra second when waiting. It’s unreliable. There’s nothing wrong with needing to do something with consistent timing.

The difference between tick() and os.clock() is that os.clock() is UTC, meaning it will be the same across all servers and clients, but tick() depends on the current timezone of the server/client that it’s used on. You don’t need to switch to os.clock() for things like game logic, but os.clock() is important for things like daily rewards.
(edit nevermind, got os.time() and os.clock() confused, use time() for game logic and os.time() for daily rewards and things similar to that)

There’s not much reason to avoid using custom heartbeat waits to replacement of wait(1), as it gives you more accuracy with little performance cost.


wait() in extreme situations can take up to 1 extra second when waiting. It’s unreliable. There’s nothing wrong with needing to do something with consistent timing.

Even more, it belongs to the secondary queue and all threads in the secondary queue are resumed after the primary queue. Not only that, it’s throttling is also FPS based.

The difference between tick() and os.clock() is that os.clock() is UTC, meaning it will be the same across all servers and clients, but tick() depends on the current timezone of the server/client that it’s used on. You don’t need to switch to os.clock() for things like game logic, but os.clock() is important for things like daily rewards.

Also know that tick will soon be deprecated, it’s obviously better to stay with what will be supported.

1 Like

Creating callbacks alone is not expensive, this can be backed by benchmarks. It can lead to performance impacts, such as memory leaks, over-connecting, embedded expensive code, etc.

Generally, using events instead of loops is considered more efficient on memory, but not always.

It’s best not to avoid threads as that can lead you down the wrong road in terms of optimization. It’s not the threads themselves, but rather, what you do with them.

In addition, connections are not threads, they are psuedo-threads, so it does work a little bit differently behind the scenes.

This sentence peaked my interest :thinking:
Where did you find this information?

1 Like

Creating a callback isn’t expensive, calling it is.

As I said, thread creation is expensive. Whatever you do in that thread determines the expensiveness of the thread, yes but do know that thread creation in general is expensive which is my point here.

Also, the information on ‘tick’ is available on the developer hub.

I did some searching and looked at the section about time on the June 2020 luau recap, and found important info.

@iGottic @SilentsReplacement The info about tick() being deprecated doesn’t seem to be on the devhub (i couldn’t find anything), but this post says they’ll be deprecating tick() in the future.

Seems like instead of os.clock() and tick(), time() should be used for game logic and os.time() should be used for daily reward systems (and things similar to that).


I believe my module would be perfectly suited in this situation: Custom wait - the best solution to yielding!
It’s basically a custom task scheduler, inspired by CloneTrooper’s tweet about how easily cloggable the task scheduler is.

1 Like

For your module should I always use this? Or only for while true do?

if debounce == false then
            debounce = true
            local Data = DataService:GetNumber(plr)
            Data.Germs += 1
            debounce = false

For example something like this.

Have you benchmarked any of this? I have boatbomber’s Benchmarker plugin so I can benchmark it later if needed.
Even then, I have a feeling that the performance cost would be so small that there’d be no reason to worry. Micro optimizations often make code look worse, and usually aren’t needed since the amount of processing they save is small.

1 Like

what about:

local RunService = game:GetService("RunService")

local Heartbeat = RunService.Heartbeat

local t = 0
local waitTime = 10

while t <= waitTime do
    t += Heartbeat:Wait()

-- or

local start = os.clock()
local duration = 10

while (os.clock() - start) < duration do

I’m just curious about it.

It should also be noted he isn’t creating a new thread. He is adding code to be ran each time the cpu cycles.

That’s how I do it but I still just do wait(). I only do the os.clock() method when im running stuff where millisecond differences matter (such as a speedrunning system)

1 Like

How not to use while wait loops: using them at all. Answered in four words. :laughing:

It’s not just while loops but loops and waits in general. Roblox developers have a bad tendency to use both of these things in cases where an event-driven system would better suit the current use case. There’s surprisingly not that many cases where you need loops, especially non-terminating ones.

I understand the need for loops and all that, but in most cases you don’t need one. RunService’s events aren’t considered loops, that pulls you into an event-driven system but you want to make sure that you’re actually using them where it counts, otherwise it’s no better than abusing the condition of while or taking advantage of the fact that wait returns truthy values.